Amin Negm-Awad Christian Kienle
Objective-C und Cocoa – Band 2: Fortgeschrittene
Objective-C und Cocoa – Band 2: Fortgeschrittene Bibliografische Information der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.ddb.de abrufbar. Copyright © 2011 SmartBooks Publishing AG ISBN: 978-3-908497-84-4 1. Auflage 2011 Projektleitung: Lektorat: Korrektorat: Layout und Satz: Covergestaltung: Coverfoto: Illustrationen: Druck und Bindung:
Horst-Dieter Radke Stefan Landvogt Dr. Anja Stiller-Reimpell Susanne Streicher Johanna Voss, Florstadt istockphoto Nr. 279738 / lisegagne © Peter Galbraith - Fotolia.com CPI books GmbH, Leck
Umwelthinweis: Dieses Buch wurde auf chlorfrei gebleichtem Papier gedruckt. Die Einschrumpffolie – zum Schutz vor Verschmutzung – ist aus umweltverträglichem und recyclingfähigem PE-Material. Trotz sorgfältigem Lektorat schleichen sich manchmal Fehler ein. Autoren und Verlag sind Ihnen dankbar für Anregungen und Hinweise! Smart Books Publishing AG http://www.smartbooks.ch Aus der Schweiz: Aus Deutschland und Österreich:
Sonnenhof 3, CH-8808 Pfäffikon SZ E-Mail:
[email protected] Tel. 055 420 11 29, Fax 055 420 11 31 Tel. 0041 55 420 11 29, Fax 0041 55 420 11 31
Alle Rechte vorbehalten. Die Verwendung der Texte und Bilder, auch auszugsweise, ist ohne die schriftliche Zustimmung des Verlags urheberrechtswidrig und strafbar. Das gilt insbesondere für die Vervielfältigung, Übersetzung, die Verwendung in Kursunterlagen oder elektronischen Systemen. Der Verlag übernimmt keine Haftung für Folgen, die auf unvollständige oder fehlerhafte Angaben in diesem Buch oder auf die Verwendung der mitgelieferten Software zurückzuführen sind. Nahezu alle in diesem Buch behandelten Hard- und Software-Bezeichnungen sind zugleich eingetragene Warenzeichen oder sollten als solche behandelt werden.
Besuchen Sie uns im Internet!
www.smartbooks.ch www.smartbooks.de
Übersicht Kapitel 1
Graphische Ausgabe
13
Kapitel 2
Nutzeraktionen: Events und Responder
Kapitel 3
View-Controller-Bindung 221
Kapitel 4
Windows, Controls und Cells
375
Kapitel 5
Core Image und Core Animation
437
Kapitel 6
Textsystem 507
Kapitel 7
Die Applikation und ihre Umgebung
553
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
697
Kapitel 9
Auslieferung, Versionierung, Konfiguration
761
149
Kapitel 10 Objective-C-Referenz 829 Index
923
Inhaltsverzeichnis Kapitel 1
Graphische Ausgabe
13
Zeichenmethode....................................................................................................................................... 16 Koordinatensystem.................................................................................................................................. 19 Maßstab ........................................................................................................................................... 19 Koordinaten, Antialiasing und Form.............................................................................................. 21 Bounds- und Frame-Koordinaten................................................................................................... 24 Änderung des Bounds-Rechteckes................................................................................................... 29 Änderung des Frame-Rechtecks....................................................................................................... 36 Scrolling............................................................................................................................................ 37 Geometrische Funktionen................................................................................................................ 39 Zeichnen................................................................................................................................................... 44 Einfache Zeichenfunktionen............................................................................................................ 45 Gradients.......................................................................................................................................... 48 Bezier-Pfade..................................................................................................................................... 52 Bezier-Kurven........................................................................................................................... 54 Unmittelbare Ausgabe von Standardelementen...................................................................... 57 Vorgefertigte Pfade................................................................................................................... 58 Pfade mit mehreren Subpfaden................................................................................................ 60 Freie Subpfade.......................................................................................................................... 61 Globale Eigenschaften.............................................................................................................. 71 Bildausgabe und NSImage............................................................................................................... 78 Zeichnen in andere Ziele.................................................................................................................. 82 Ausgabe in Images.................................................................................................................... 82 Fullscreen.................................................................................................................................. 83 Graphischer Kontext........................................................................................................................ 85 Funktion des Kontextes............................................................................................................ 85 Globale Zeichenattribute.......................................................................................................... 86 Transformationen.................................................................................................................................... 91 Grundlagen...................................................................................................................................... 91 Transformationen in Cocoa............................................................................................................. 94 Zwischenspurt: Strukturierung......................................................................................................100 View-Hierarchien ..................................................................................................................................107 Erzeugen im Code..........................................................................................................................108 Sizing..............................................................................................................................................110 Pixelgenaues Zeichnen..........................................................................................................................113 Zeichenanforderung..............................................................................................................................119 Drucken.................................................................................................................................................132 Page-Layout....................................................................................................................................132 Print-Operation .............................................................................................................................135 Paginierung....................................................................................................................................138 Core Graphics........................................................................................................................................143 Core-Graphics-»Objekte«..............................................................................................................143 Objektorientierte Behandlung von Strukturen......................................................................143 C-»Objekte«............................................................................................................................144 Umwandlung..................................................................................................................................146 Umwandlungsfunktionen.......................................................................................................146 Manuelles Umwandeln...........................................................................................................147 Toll-Free-Bridging...................................................................................................................147 Zusammenfassung.................................................................................................................................148
Kapitel 2
Nutzeraktionen: Events und Responder
149
Events im System von Cocoa.................................................................................................................151 Events und Responder....................................................................................................................151 Event-Dispatch...............................................................................................................................152 Eventmethode und Responder-Chain...........................................................................................157 Mausevents............................................................................................................................................161 Dispatch .........................................................................................................................................161 Eventmethoden...............................................................................................................................163 Klick-Methoden......................................................................................................................163 Drag-Methoden......................................................................................................................171 Drag & Drop..................................................................................................................................185 Drag-Zyklus starten................................................................................................................187 Dragging-Source.....................................................................................................................192 Dragging-Destination.............................................................................................................196
Cursor-Rects...........................................................................................................................202 Tracking-Areas........................................................................................................................203 Mouse-Move-Event.................................................................................................................206 Tastaturevents........................................................................................................................................207 Eventdispatch.................................................................................................................................208 Eventmethoden...............................................................................................................................209 Tastendruck............................................................................................................................209 Modifier..................................................................................................................................212 Actions...................................................................................................................................................215 Applikation als Verteiler................................................................................................................215 Erster Versuch: Responder-Chain des Key-Windows............................................................217 Zweiter Versuch: Responder-Chain des Main-Windows......................................................217 Dritter Versuch: Applikation..................................................................................................218 Zusammenfassung.................................................................................................................................219
Kapitel 3
View-Controller-Bindung 221 Einleitung...............................................................................................................................................222 Model erstellen...............................................................................................................................222 Datenfluss.......................................................................................................................................230 Kontrollfluss...................................................................................................................................231 Viewhierarchien.............................................................................................................................231 Data-Source...........................................................................................................................................232 Data-Source anbieten.............................................................................................................232 Data-Source implementieren.................................................................................................236 Data-Source nutzen................................................................................................................239 Umwandlung..........................................................................................................................250 Key-Value-Observing und Bindings.....................................................................................................252 Key-Value-Observing.....................................................................................................................256 Attribute..................................................................................................................................258 Attribute hinter Master-Detail-Beziehungen........................................................................266 To-many-Relation mit NSSet ................................................................................................271 To-many-Relation mit Array.................................................................................................282 Attribute hinter To-many-Relationen....................................................................................296 Cocoa-Bindings..............................................................................................................................301 Bindings und Key-Value-Observing .....................................................................................301 Binding-Optionen (Value-Transformer)...............................................................................318 Bindings-Controller................................................................................................................323 Berechnete Eigenschaften.......................................................................................................334 Delegating..............................................................................................................................................349 Definition der API..........................................................................................................................350 Delegating implementieren............................................................................................................354 Klick auf Depot: neue Karten.................................................................................................354 Dragging starten.....................................................................................................................358 Pasteboard füllen....................................................................................................................364 Drag, Drag-Over und Drop....................................................................................................367 Zusammenfassung.................................................................................................................................373
Kapitel 4
Windows, Controls und Cells
375
Controls und Cells.................................................................................................................................376 Control............................................................................................................................................377 Cell..................................................................................................................................................382 Cell anmelden.........................................................................................................................383 Cells zeichnen.........................................................................................................................384 Umrandung zeichnen.............................................................................................................386 Inhalte zeichnen......................................................................................................................387 Events abarbeiten...........................................................................................................................390 Kommunikation der Cell mit dem Control...........................................................................391 Kommunikation des Controls mit der Cell............................................................................391 Mehrere Panes verwalten...............................................................................................................395 Tableviews, -columns und Cells............................................................................................................398 Columns.........................................................................................................................................398 Column-Cells..................................................................................................................................401 Kombinierte Cells...........................................................................................................................409 Control und Events................................................................................................................................420 Löschtaste empfangen....................................................................................................................422 Löschung durchführen...................................................................................................................422 Windows................................................................................................................................................424 Subklassen für Fenster und View..................................................................................................424 Transparenz....................................................................................................................................426
Konfiguration.................................................................................................................................428 Field-Editor....................................................................................................................................432 Anpassung...............................................................................................................................433 Zusammenfassung.................................................................................................................................435
Kapitel 5
Core Image und Core Animation
437
Core Image.............................................................................................................................................438 Die Anwendung Core Image Fun House.......................................................................................440 Das Handwerkszeug: die Core Image-Klassen..............................................................................441 Core Image-Filter nutzen...............................................................................................................443 Hintergründe: Wieso Core Image so schnell ist.............................................................................450 Mehrere Filter kombinieren...........................................................................................................452 Composite-Operation-Filter...................................................................................................452 Filter verketten........................................................................................................................458 Fazit und Ausblick.................................................................................................................................461 Core Animation.....................................................................................................................................463 Der Animator eines Views.............................................................................................................463 Das Protokoll NSAnimatablePropertyContainer.........................................................................466 Die Animationsklassen..................................................................................................................467 Die Layerklassen............................................................................................................................468 Layer-backed-Views.......................................................................................................................469 Eine Bildershow mit Core Animation...........................................................................................469 Eigene Eigenschaften eines Views animieren................................................................................479 Den Hintergrundfilter animieren..................................................................................................484 Alles zusammen: Core Image und Core Animation.....................................................................489 Mehr über Layer im Zusammenspiel mit Views...........................................................................491 Der Inhalt eines Layers..................................................................................................................492 Der Nutzen von Layern.................................................................................................................495 Mehrere Layer in einem View........................................................................................................497 Transformationen in 3D................................................................................................................502
Kapitel 6
Textsystem 507 Zeichen- und Fonts................................................................................................................................510 Übersicht.........................................................................................................................................510 Zeichen....................................................................................................................................510 Zeichenkodierung...................................................................................................................510 Glyphen...................................................................................................................................516 Fonts........................................................................................................................................516 Fontmanager..................................................................................................................................517 Fonts, Ausgabe und Bemaßung.....................................................................................................520 Layoutsystem.........................................................................................................................................525 Layoutstapel...................................................................................................................................526 Layoutstapel erstellen.....................................................................................................................528 Textview.........................................................................................................................................532 Textcontainer..................................................................................................................................534 Textstorage......................................................................................................................................544 Text und Attribute..................................................................................................................545 Attribute setzen.......................................................................................................................549 Eigene Attribute......................................................................................................................551 Zusammenfassung.................................................................................................................................552
Kapitel 7
Die Applikation und ihre Umgebung
553
Das Dateisystem....................................................................................................................................554 Grundlagen....................................................................................................................................554 Low-Level: Knoten und Links................................................................................................555 Verzeichnisdateien..................................................................................................................557 Links........................................................................................................................................559 Verzeichnisstruktur und Mounts...........................................................................................565 Domains..................................................................................................................................567 Lokalisierte Pfade...................................................................................................................568 Path-Services..................................................................................................................................569 Filemanager....................................................................................................................................571 Verzeichnisse lesen..................................................................................................................572 Verzeichnis- und Dateioperationen.......................................................................................575 Dateiattribute.........................................................................................................................576 File-Wrappers.................................................................................................................................576 File-Handles und Sockets...............................................................................................................577 Dateien synchron lesen...........................................................................................................577 Dateien asynchron lesen.........................................................................................................579
Workspace..............................................................................................................................................581 Notification-Center........................................................................................................................581 Dateioperationen...........................................................................................................................586 Asynchrone Methoden............................................................................................................586 Synchrone Methoden..............................................................................................................588 Application.............................................................................................................................................589 Dock................................................................................................................................................589 Dockbadge...............................................................................................................................590 Dockimage..............................................................................................................................590 Dockview.................................................................................................................................590 Dockmenü...............................................................................................................................596 Aktivierung.....................................................................................................................................598 Delegatemethoden..................................................................................................................598 Anforderung............................................................................................................................601 Terminierung..........................................................................................................................604 Statusmenübar...............................................................................................................................605 Statusmenü.............................................................................................................................606 Applikationsmenü und Dock ausblenden.............................................................................612 Plugins....................................................................................................................................................613 Formulierung einer Plugin-API.....................................................................................................614 Das Spezielle an Cocoa-Plugins.....................................................................................................615 Integration in eine Anwendung.....................................................................................................615 Entwicklung des abstrakten Plugin-Interfaces.......................................................................615 Implementierung eines Plugins..............................................................................................618 Laden von Plugins..................................................................................................................623 Zusätzliche Hinweise.....................................................................................................................630 Scripting.................................................................................................................................................631 Skriptfähigkeit................................................................................................................................632 Die Standardsuite...........................................................................................................................632 Anwendungsspezifische Unterstützung.........................................................................................633 Grundfunktionen publizieren und eigene Suite definieren...................................................634 Eigenschaften publizieren.......................................................................................................635 Listen publizieren...................................................................................................................637 Weitere Hinweise....................................................................................................................640 Die Scripting Bridge.......................................................................................................................641 Das Handwerkszeug: die wichtigsten Klassen der Scripting Bridge.....................................642 Hinter den Kulissen................................................................................................................646 iPhoto-Browser und die Launch Flags von SBApplication...................................................647 Fazit........................................................................................................................................659 Internetanbindung.................................................................................................................................659 Klassensystem.................................................................................................................................659 Anwendung anhand eines REST-Services ....................................................................................662 Merkmale und Eigenschaften von REST-Webservices..........................................................663 Webservices aus Sicht eines Cocoa-Programmierers.............................................................664 Anfragen schicken und auswerten.........................................................................................665 Parametrisierte Anfragen..............................................................................................................668 Parametrisierte URLs.............................................................................................................669 Formulieren der Anfrage........................................................................................................670 Das Encoding einer Antwort..................................................................................................670 Asynchrone Anfragen.....................................................................................................................671 Asynchrones Ermitteln der neuesten Lesezeichen ................................................................673 Datenformat der Antworten und Anfragen..........................................................................676 Antwort in Objekte transformieren.......................................................................................677 Schreibende Anfragen....................................................................................................................682 Komplexe REST-Webservices in der Praxis..................................................................................684 Die Operatoren.......................................................................................................................684 Arten der Authentifizierung...................................................................................................686 Authentifizierung bei Blogger.com.........................................................................................687 Informationen verfügbarer Blogs abrufen ............................................................................691 Neuen Eintrag erzeugen.........................................................................................................692 Zusätzliche Hinweise.....................................................................................................................696 Weitere Frameworks..............................................................................................................................696 Zusammenfassung.................................................................................................................................696
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
697
Grundlagen............................................................................................................................................698 Probleme der Nebenläufigkeit........................................................................................................700 Arten von Nebenläufigkeit.............................................................................................................701 Run-Loops..............................................................................................................................................706 Modes und Input-Sources..............................................................................................................706
Timer..............................................................................................................................................708 Nutzung von Timern..............................................................................................................708 Timer und Run-Loop-Modes.................................................................................................713 Modal-Sessions...............................................................................................................................714 Netzwerkevents..............................................................................................................................718 Threads...................................................................................................................................................721 Manuelle Threads...........................................................................................................................721 Thread erzeugen.....................................................................................................................722 Thread beenden......................................................................................................................725 Threadfestigkeit von Cocoa............................................................................................................727 Threads und AppKit...............................................................................................................727 Threads und Core Data..........................................................................................................731 Synchronisierungen und Locking..................................................................................................732 Locking mit Objective-C.........................................................................................................735 Locking mit Cocoa .................................................................................................................738 Operation-Queues..........................................................................................................................740 Operation-Queues und eigene Operation-Subklassen.................................................................742 Tasks.......................................................................................................................................................754 Erzeugen und Ausführen von Tasks......................................................................................754 Launchpfad, Arbeitsverzeichnis und Argumente..................................................................754 Ausgabe eines Tasks empfangen.............................................................................................756 Asynchrones empfangen.........................................................................................................758
Kapitel 9
Auslieferung, Versionierung, Konfiguration
761
Softwareversionen..................................................................................................................................762 Präprozessor...................................................................................................................................762 Defines.....................................................................................................................................763 Bedingte Kompilierung...........................................................................................................765 Xcode-Unterstützung.....................................................................................................................769 Defines aus dem Build-Prozess..............................................................................................769 Build-Konfigurationen...........................................................................................................772 Target-Konfiguration..............................................................................................................773 Versionierung von Models und Datenmigration mit Core Data.........................................................775 Versionierung von Models.....................................................................................................................776 Übersicht.........................................................................................................................................776 Modelversionen erzeugen.......................................................................................................776 Mappingmodels......................................................................................................................777 Die Mappingklassen...............................................................................................................778 Automatische Migration................................................................................................................780 Erzeugen eines versionierten Models sowie eines Mappingmodels......................................782 Der Migrationsprozess............................................................................................................785 Funktionsweise von Propertymappings ................................................................................786 Eigene NSEntityMigrationPolicy...................................................................................................787 Manuelle Migration.......................................................................................................................790 Kompatibilitätskonflikte erkennen.........................................................................................791 Anstoßen eines Migrationsprozesses......................................................................................791 Fazit................................................................................................................................................791 Frameworks...........................................................................................................................................791 Einsatzgebiete.................................................................................................................................792 Framework erstellen.......................................................................................................................793 Framework nutzen.........................................................................................................................796 Framework erweitern.....................................................................................................................797 Gradient View nutzen....................................................................................................................801 Interface Builder-Plugin für das Gradient View...........................................................................802 Projekt anlegen.......................................................................................................................802 Gradient View übernehmen...................................................................................................803 Platzhalter anpassen..............................................................................................................804 Inspektor implementieren.......................................................................................................804 Plugin ausliefern.....................................................................................................................805 Plugins in der Praxis..............................................................................................................806 Sourcekontrollsysteme...........................................................................................................................807 Konzept...........................................................................................................................................807 Anlegen von Archiv und Projekt....................................................................................................809 Anlegen des Repositorys.........................................................................................................809 Repository in Xcode anmelden...............................................................................................810 Projekt erzeugen und mit dem Repository verbinden...........................................................811 Lokal arbeiten................................................................................................................................813 Eigene Änderungen vornehmen und hochladen...................................................................815 Fremde Änderungen herunterladen......................................................................................816 Kollision..................................................................................................................................816
Änderungen der Projektstruktur............................................................................................818 Lokalisierungen......................................................................................................................819 Rollback...................................................................................................................................819 Zusammenfassung..................................................................................................................820 Snapshot.........................................................................................................................................820 Testen.....................................................................................................................................................820 Instruments....................................................................................................................................821 Speicherlöcher finden.............................................................................................................822 Ausblick...................................................................................................................................825 Automatisiertes Testen...................................................................................................................825 Einen Testfall erzeugen...........................................................................................................825 Einen Test erzeugen................................................................................................................826 Zusammenfassung.................................................................................................................................828
Kapitel 10 Objective-C-Referenz 829 (Sprach-)Versionen................................................................................................................................830 Einfache Typen......................................................................................................................................830 Ganzzahlen....................................................................................................................................830 char, short, int, long, long long, size_t, fpos_t, off_t..............................................................830 BOOL, YES, NO.....................................................................................................................831 NSInteger, NSUInteger...........................................................................................................831 unichar, FourCharCode.........................................................................................................831 Literale Konstanten................................................................................................................831 Speicherplatz und Wertebereiche...........................................................................................832 Interne Darstellung, Binärsystem, Oktalsystem, Hexadezimalsystem.................................834 Fließkommazahlen.........................................................................................................................836 Zeiger..............................................................................................................................................837 Typisierter Zeiger....................................................................................................................837 id, nil.......................................................................................................................................838 Class, Nil.................................................................................................................................838 Protocol...................................................................................................................................838 Funktionszeiger.......................................................................................................................839 IMP.........................................................................................................................................839 Blockreferenzen.......................................................................................................................840 Untypisierter Zeiger................................................................................................................840 SEL..................................................................................................................................................840 Aufzählungstyp enum....................................................................................................................841 Strukturtyp struct..........................................................................................................................842 Unionstyp union............................................................................................................................843 C-Arrays.........................................................................................................................................844 Typdefinitionen..............................................................................................................................847 Speicherklassen und Qualifizierer.................................................................................................849 const........................................................................................................................................849 Lokales static und auto...........................................................................................................850 Globales static.........................................................................................................................852 extern......................................................................................................................................853 register.....................................................................................................................................854 __block....................................................................................................................................855 Typcasting.......................................................................................................................................855 Ausdrücke.......................................................................................................................................856 Operanden..............................................................................................................................856 Algebraische Operatoren........................................................................................................857 Vergleichsoperatoren..............................................................................................................858 Boolsche Operatoren...............................................................................................................858 Bitoperatoren..........................................................................................................................859 Adress- und Derefenrenzierungsoperatoren..........................................................................862 Aufzählungsoperator..............................................................................................................863 Größenoperator......................................................................................................................863 Bedingter Ausdruck................................................................................................................863 Zuweisungsoperatoren............................................................................................................864 lvalue.......................................................................................................................................864 Ausführungsoperator..............................................................................................................865 Klammeroperator, Priorität...................................................................................................865 Klassen...................................................................................................................................................866 Deklaration.....................................................................................................................................867 Klassenangabe........................................................................................................................867 Instanzvariablen.....................................................................................................................868 Methoden................................................................................................................................870 Eigenschaften..........................................................................................................................871 Forward-Declaration..............................................................................................................874
Class-Continuation........................................................................................................................874 Implementierung............................................................................................................................875 Methodendefinition................................................................................................................875 Eigenschaftsdefinition.............................................................................................................875 Kategorien..............................................................................................................................................877 Protokolle...............................................................................................................................................877 Definition.......................................................................................................................................877 Protokollangabe......................................................................................................................878 Methoden und Propertys........................................................................................................878 Optionen.................................................................................................................................878 Forward-Declaration..............................................................................................................878 Verwendung als Typ.......................................................................................................................879 Verwendung zur Laufzeit..............................................................................................................879 Ausführungseinheiten...........................................................................................................................879 Anweisung......................................................................................................................................880 C-Blöcke..........................................................................................................................................880 Kontrollstrukturen ........................................................................................................................880 Bedingte Anweisung...............................................................................................................881 Mehrfachauswahl...................................................................................................................882 while-Wiederholung...............................................................................................................882 do-Wiederholung....................................................................................................................883 for-Wiederholung...................................................................................................................883 for-in-Wiederholung...............................................................................................................884 break........................................................................................................................................884 continue...................................................................................................................................884 Funktionen.....................................................................................................................................884 Deklaration und Definition....................................................................................................884 return......................................................................................................................................885 Blocks..............................................................................................................................................885 Closure....................................................................................................................................886 Variablen.................................................................................................................................886 Lebensdauer............................................................................................................................888 Nachrichten............................................................................................................................................888 Versand...........................................................................................................................................888 Klassisch..................................................................................................................................890 Perform-Selector-Methoden...................................................................................................890 NSInvocation-Methoden........................................................................................................890 Dispatching.....................................................................................................................................891 Dynamisch gebundene Nachrichten......................................................................................891 Statisch gebundene Nachrichten............................................................................................897 Gescheiteter Dispatch.............................................................................................................902 Dynamische Methodenerzeugung.........................................................................................903 Exceptions..............................................................................................................................................903 Exceptions werfen..........................................................................................................................904 Exceptions fangen..........................................................................................................................905 Exceptions zurückwerfen...............................................................................................................907 Objekterzeugung und –zerstörung........................................................................................................907 Klassenobjekte................................................................................................................................908 Allokation...............................................................................................................................908 Initialisierung..........................................................................................................................908 Zerstörung...............................................................................................................................909 Instanzerzeugung...........................................................................................................................909 Allokation...............................................................................................................................909 Initialisierungsreihenfolge......................................................................................................910 Inhaltliche Initialisierung.......................................................................................................912 Zerstörung...............................................................................................................................913 Laufzeitsystem.......................................................................................................................................914 Überblick........................................................................................................................................914 Instanz- und Klasseninformationen..............................................................................................915 Instanzobjekte.........................................................................................................................916 Klassenobjekte........................................................................................................................917 Metaklassenobjekte.................................................................................................................918 Protokolle........................................................................................................................................920 Nachrichtenversand.......................................................................................................................920 Zusammenfassung.................................................................................................................................921
Kapitel 11 Index 923
Graphische Ausgabe
1
Im Band 1 haben Sie ausführlich die Möglichkeit existierender Views kennengelernt. In den nächsten Kapiteln geht es darum, eigene Views durch Subklassen zu programmieren. Der erste Kernbereich behandelt die Frage, wie man eigene Darstellungen auf den Bildschirm zaubert.
SmartBooks
Objective-C und Cocoa – Band 2
Graphische Ausgabe Die Bildschirmausgabe (und auch die auf dem Drucker) erfolgt stets in Views. Alles, was Sie auf dem Bildschirm sehen, ist Inhalt eines Views. Im ersten Band sind zahlreiche Views und Controls von Cocoa verwendet worden. In den meisten Applikationen reicht dies aus. Zuweilen muss man jedoch Viewklassen durch Ableitung anpassen.
•
Es sollen eigene Darstellungen von Daten erfolgen. Hier ist vor allem an Applikationen zu denken, die die Graphik in den Mittelpunkt stellen wie Zeichenprogramme. Aber manchmal will man auch lediglich die Art des Zeichnens eines vorhandenen Standardviews ein wenig verändern.
•
Es sollen Nutzereingaben (Events) wie Mausklicks und Tastendrücke auf eigene Art entgegengenommen werden. Dies ergibt sich häufig aus dem ersten Punkt, wenn etwa Linien gezeichnet werden sollen. Aber auch hier gibt es Beispiele für das Verändern bestehender Views. Ein Klassiker ist die Löschtaste in einem Tableview. Hiermit beschäftigen wir uns im nächsten Kapitel.
•
Nicht hierher gehört in der Regel der Fall, dass zwar ein Event wie üblich entgegengenommen werden soll, jedoch die applikationsspezifische Aktion programmiert werden muss. So etwas lässt sich in aller Regel im Controller implementieren, sei es als Action-Target-Controller, sei es als Delegate.
BEISPIEL Der letzte Punkt wird vielleicht klar, wenn man sich Drag & Drop vorstellt: Was genau gezogen wird und wohin es gezogen wurde, kann nur das View wissen, weil dieses sein Layout der Daten kennt. Ob ein solches Dragging zulässig ist und zu welcher Aktion es führt, dürfte aber in der Regel die Controllerschicht betreffen. Hier geht es also um die Schnittstelle zwischen View und Controller, nicht um die Darstellung im View an sich. Diese Problematik wird daher in einem eigenen Kapitel besprochen. Fangen wir einfach gleich wieder an: Legen Sie sich bitte ein neues Projekt der Art Application | Cocoa Application ohne Unterstützung von Dokumenten und Core Data an und benennen Sie es als »Card Game«. Sie ahnen schon, wohin die Reise geht: Wir zeichnen einen grünen Kartentisch mit Kartenstapeln darauf. Als Nächstes erzeugen wir unsere eigene Subklasse von NSView. Hierzu legen wir in Xcode mit File | New File… eine eigene Klasse für den View an. Wählen Sie als Vorlage – und das ist neu – allerdings Objective-C class | NSView und benennen Sie diese mit TableBaizeView. 14
Kapitel 1
Graphische Ausgabe
Wechseln Sie in den Interface Builder und öffnen Sie dort das Fenster Window (Card Game) im Hauptfenster. Ziehen Sie nun aus der Library unter Cocoa | Views & Cells | Layout Views das Custom View in das Fenster und platzieren Sie es in der linken, oberen Ecke des Fensters mit dem vom Interface Builder vorgegebenem Rand. Geben Sie als Breite und Höhe jeweils 200 ein. Setzen Sie außerdem das Autoresizing so wie abgebildet. Nun ziehen Sie das Fenster kleiner, bis wieder die automatischen Hilfslinien für die Umrandung erscheinen. Sie sollten jetzt also einen View mit der Größe 200 x 200 haben, der an jeder Seite einen Rand von 20 hat. Sie können die Größe des Contentviews im Fenster mit 240 x 240 überprüfen, indem Sie in den Hintergrund klicken und in das Size-Pane des Inspectors schauen. Contentview 240 x 240
Custom-View 200 x 200
Die Lage des Customviews im Koordinatensystem des Contentviews.
15
SmartBooks
Objective-C und Cocoa – Band 2
Zeichenmethode Ein View kann als Subklasse von NSResponder Events des Benutzers empfangen, also insbesondere Mausklicks, Tastendrücke usw. Bereits im ersten Band wurde ausgeführt, dass Views sich zeichnen, wenn Sie vom System ein »Neuzeichnen-Ereignis« bekommen. Ohne dem nächsten Kapitel vorzugreifen, führt dies dazu, dass eine bestimmte Methode aufgerufen wird, die dann dafür verantwortlich ist, den kompletten View zu zeichnen: -drawRect: Sie können diese Methode auch bereits in dem Code sehen. Schauen Sie in TableBaizeView.m: - (void)drawRect:(NSRect)rect { // Drawing code here. }
Wenn Sie also verstanden haben, dass Sie nicht einfach neu zeichnen, wenn es Ihnen gerade passt, sondern sämtlichen Ausgabecode hier unterbringen, haben Sie bereits die erste klassische Hürde genommen.
GRUNDLAGEN Es ist grundsätzlich möglich, auch außerhalb von -drawRect: (NSView) in einen View zu malen. Davon raten wir aber dringend ab. Wenn schon wie im ersten Band besprochen, ein unmittelbares -display (NSView) eine Sünde ist, so ist ein unmittelbares Zeichnen in einen View eine Todsünde. Vertrauen Sie dem System! Es weiß schon, wann was gezeichnet werden muss. Kommen wir zu dem erstaunlichen Parameter rect: Dieser gibt an, welcher Bereich des Views neu gezeichnet werden muss. (Wie wir später noch sehen werden, bestimmt das View selbst, welche Bereiche dies sind. Diese Information wandert allerdings einmal durch das System.) So ist etwa der Inhalt eines Tableviews immer nur teilweise sichtbar, so dass es nutzlos wäre, es vollständig neu zu zeichnen, um es zu aktualisieren. Und noch mehr: Wenn sich ein einzelner Wert des Tableviews ändert, muss nicht der gesamte Tableview neu gezeichnet werden, sondern nur der einzelne Wert. Dies gilt auch für selbst gebastelte Views: Stellen Sie sich einen View vor, der verschiedene graphische Symbole enthält. Von zweien dieser Symbole wird nun die Farb-Eigenschaft geändert. Also muss der Bereich dieser beiden Symbole zum Neuzeichnen markiert werden.
16
Kapitel 1
Graphische Ausgabe
Fenster
Fenster
Je nach Anforderung sind unterschiedliche Bereiche zu zeichnen.
Im linken Beispiel ergeben sich zwei Bereiche zum Aktualisieren: die Ellipse und das Kreuz. Hieraus ergibt sich ein umfassendes Rechteck, welches sämtliche neu zu zeichnenden Bereiche enthält. In diesem Falle liegen sämtliche Elemente in diesem Bereich. Rechts indessen wurden das Dreieck und die Ellipse geändert. Beide müssen aktualisiert werden. Das gemeinsame Rechteck ist indessen deutlich kleiner und enthält vor allem nicht die unteren Elemente. Es reicht also aus, einen Teil zu zeichnen. Die Mitteilung, dass ein Bereich ungültig geworden ist, lässt sich über Methoden des Views vornehmen:
•
-setNeedsDisplay: bekommt einen Parameter, der eigentlich immer YES ist, und sorgt dafür, dass das gesamte View neu gezeichnet wird. Das ist also die einfache Methode.
•
Mit -setNeedsDisplayInRect: kann indessen ein bestimmter Bereich zum Neuzeichnen markiert werden. Wenn wir etwa Karten von einem Stapel entfernen oder welche hinzufügen oder einzelne Karten ändern, dann ist es ja nicht notwendig, den gesamten Stapel neu zu zeichnen. Vielmehr markieren wir den Bereich der einzelnen Karten – am besten sogar nur den sichtbaren, also denjenigen, der nicht durch andere Karten verdeckt ist.
Der Hintergrund für dieses Vorgehen ist, dass es zuweilen viel Speicher und Rechenleistung kostet, Dinge zu zeichnen. Man sagt, dass die Operation teuer ist. Wenn ich Informationen darüber erhalte, was ich neu zeichnen muss, kann ich mir 17
SmartBooks
Objective-C und Cocoa – Band 2
vielleicht die eine oder andere Operation sparen. Dementsprechend existieren drei Strategien für die Implementierung von -drawRect:
•
Bei einer Zeichenanforderung wird stets das gesamte View neu gezeichnet. Teuer, aber einfach.
•
Ich male nur das umfassende Rechteck neu. Dieses Rechteck wird über den Parameter rect mitgeteilt: Mittelpreisig, aber schwieriger zu implementieren.
•
Ich frage ausdrücklich nach, welche Bereiche markiert wurden, und zeichne nur diese neu. Die markierten Bereiche erhält man mit -getRectsBeingDrawn: count: (NSView): billig, aber aufwendig zu implementieren.
Auch hier gilt wieder der Satz von Donald Knuth: »Premature optimization is the root of all evil.« Machen Sie es zunächst einfach und verbessern Sie Ihre Applikation dann, wenn Sie bemerken, dass es notwendig ist. Wir werden uns am Ende dieses Kapitels mit den Zeichenanforderungen im Einzelnen beschäftigen. Aber zunächst wollen wir überhaupt etwas Sinnvolles auf den Schirm bringen.
18
Kapitel 1
Graphische Ausgabe
Koordinatensystem Damit gezeichnet werden kann, muss eine Angabe dazu vorhanden sein, wo gezeichnet wird. Wir werden gleich einen Kreis auf den Bildschirm bringen. Dieser Kreis hat eine Lage und eine Ausdehnung. Die Angaben erfolgen dabei bei Cocoa in einem kartesischen Koordinatensystem, welches also X- und Y-Koordinaten kennt. Sie kennen das von Funktionsgraphen aus der Schule.
Maßstab Wenn wir Koordinaten verwenden, stellt sich immer die Frage, in welchem Maßstab sie vorliegen. Zunächst: Standardmäßig liegt der Ursprung des Koordinatensystems eines Views unten links, so dass sich in Y-Richtung die Koordinaten nach oben vergrößern. In Cocoa sind alle Koordinaten Fließkommazahlen des Typs CGFloat. Als Maßeinheit sollten Sie von 72 dpi ausgehen, also jede Einheit als ein 72-stel Zoll betrachten. Man nennt diese Einheit einen »(typographischen) Punkt mit dem Einheitszeichen p«. Die Ausgabe auf dem Bildschirm erfolgt tatsächlich nicht mit dieser Größe. Bis OS X 10.4 entsprach eine Einheit stets einem Pixel auf dem Bildschirm, auch dann, wenn dessen Auflösung nicht 1/72 Zoll betrug. Bei den meisten Modellen ist das auch unter OS X 10.6 noch so. Allerdings darf man diese Annahme nicht treffen, da es möglich ist, dass diese Einheit vor der Zeichenausgabe skaliert wird, so dass eine Einheit nicht mehr einer Pixelgröße entspricht. In jedem Falle (mit Skalierung oder ohne) ist es im Unterschied zu klassischen »Computerkoordinaten« nicht so, dass die Koordinatenangaben unmittelbar Bildschirmpunkte (Pixel) adressieren. Sogar dann, wenn Einheiten eins-zu-eins auf Pixel abgebildet werden. Der Grund liegt darin, dass wir es bei den Koordinaten mit Fließkommazahlen zu tun haben: Wer schon länger programmiert, wird vielleicht noch im Kopf haben, dass die auf dem Bildschirm vorhandenen Pixel mit Ganzzahlen durchnummeriert sind. Diese Koordinaten adressieren also gar nicht Punkte im mathematischen Sinne, denn diese haben keine Ausdehnung. Vielmehr werden Flächen bestimmt. Die englische Bezeichnung Pixel (für Picture Element) ist also treffend, nicht die deutschen Bildschirmpunkte, die Bildschirmflächen heißen müssten. Cocoa nimmt es indessen mit Bildschirmpunkten ernst und adressiert wirklich den Schnittpunkt ohne (geometrische) Ausdehnung. 19
SmartBooks
Objective-C und Cocoa – Band 2
5
4 3
4
{2,3}
3
2
{2,3}
2
1
1
0 0
2
1
3
0
4
0
2
1
3
4
5
Tradition und Moderne: Bei Cocoa (rechts) sind Koordinaten Fließkommazahlen und adressieren daher Punkte im mathematischen Sinne.
Hieraus folgt zum einen, dass das Koordinatensystem traditionell um einen halben Pixel verschoben ist. Der Mittelpunkt der unteren, linken Fläche hat die Koordinaten { 0.5, 0.5 }. Da aber keine Flächen, sondern (mathematische) Punkte angegeben werden, folgt zudem, dass jedes Objekt, welches auf dem Bildschirm gezeichnet wird, eine Ausdehnung haben muss. Die Ausdehnung kann sich ja nicht mehr »stillschweigend« aus der Pixelgröße ergeben, existieren doch für den Programmierer keine Pixel. 5
5
4
4
3
3
2
2
1
1
0
0
1
2
3
1p
20
4
0
0
1
2 2p
3
4
Kapitel 1
Graphische Ausgabe
Dies gilt sogar für Linien: Bei klassischen Koordinaten waren ja die Pixelflächen durch die Verwendung von Ganzzahlen impliziert. Das kann man nunmehr nicht mehr sagen, da eine Fließkommakoordinate in einer Fließkommazahl keine Ausdehnung hat. Es muss daher die Breite einer solchen Linie angegeben werden, wobei Cocoa 1 p (also, 1/72 Zoll, jedoch nicht: 1 Pixel!) als Standardwert nimmt. Dies kann verändert werden, wobei wiederum Fließkommazahlen Anwendung finden. Eine Linie mit einer Breite von 1,3 p ist also kein Problem.
Koordinaten, Antialiasing und Form Ein weiteres Problem ergibt sich daraus, dass Linien nunmehr nicht genau auf Pixeln liegen müssen. Je nach Koordinaten werden die einzelnen Pixel nur teilweise überdeckt. Cocoa gleicht dies aus, indem bei nur teilweiser Abdeckung die einzelnen Pixel abgeschwächt eingefärbt werden, vereinfacht im Anteil des Überdeckungsgrades. Man nennt dies »Antialiasing«, und dies führt zu den verwischten Linien, die man manchmal wahrnimmt. { 2.0, 6.0 }
8
{ 4.5, 6.0 }
{ 6.5, 6.5 }
7 6 5 4 3 2 1 0
0
1
2
3
4
5
6
7
8
Linienbreite: 1 p Knapp daneben ist auch vorbei: Eher zufällig werden beim Zeichnen Pixel getroffen.
21
SmartBooks
Objective-C und Cocoa – Band 2
Dieser Effekt ist übrigens nicht nur durch die Fließkommakoordinaten bedingt. Vielmehr existierte er schon immer bei schrägen Linien (die ja auch nicht komplett auf Pixeln liegen können), gekrümmten Linien und bei Schriften.
Mit und ohne Antialiasing: Vergrößert glaubt man gar nicht, dass in der Regel die Darstellung mit Antialiasing besser lesbar ist.
GRUNDLAGEN Die obige Darstellung kommt bei Ihnen übrigens nicht richtig an: Quartz, das graphische Ausgabesystem von OS X, verwendet nämlich Subpixelantialiasing, was bedeutet, dass die auf dem Bildschirm nebeneinander platzierten drei Farben Rot, Grün und Blau sozusagen als »Drittelpixel« in die Berechnung einbezogen werden können. Im Original sind daher die »verwischten« Pixel farbig. Manche Menschen haben damit ein Problem. Sie können sich eine pixelweise Vergrößerung des Bildschirms übrigens mit dem mit den Developer Tools mitgelieferten Programm »Pixie« anzeigen lassen. Einfach mal in Spotlight danach suchen. Dieses Programm verwenden Sie bitte ständig, um die von uns gemachten Ausgaben in diesem Kapitel auch noch einmal im Detail zu betrachten. Sie entwickeln damit ein Gefühl für die Angelegenheit. Linienstärke und Formen Da Fließkommazahlen keine Ausdehnung kennen, muss jede gezeichnete Linie eine Liniendicke oder -stärke haben. Standardmäßig ist das 1 p.
GRUNDLAGEN Es sei noch angemerkt, dass eine Liniendicke von 0.0 Cocoa veranlasst, die dünnst-mögliche Linie für das Ausgabegerät zu zeichnen. Dies entspricht dann auf dem Bildschirm in der Regel einem Pixel. Aber auch hier Vorsicht mit Annahmen: Stellen Sie sich einen Plotter vor, der zwar über eine sehr feine Auflösung verfügt, in dem sich aber nur dicke Stifte befinden. Dessen dünst-mögliche Linie verhält sich zur Auflösung wie die Fläche des Saarlandes zu einer Kirsche. (Man kann das übrigens auch in Höhen des Kölner Doms oder in Güterzügen, die um die Erde fahren, umrechnen.)
22
Kapitel 1
Graphische Ausgabe
Wichtig ist dabei, dass eine Linie von der gedachten Verbindungsstrecke zwischen den angegebenen Koordinaten in beide Richtungen wächst. Bedenken Sie etwa unser obiges Beispiel: Hier hatten wir die verschiedenen Bereiche von Symbolen zum Neuzeichnen markiert. Die Ellipse befand sich etwa in einem Rechteck, welches durch ihre Radien bestimmt war. Aber dieses Rechteck vergrößert sich noch um die Hälfte der Liniendicke. Wenn wir also das Objekt zum Neuzeichnen markieren, müssen wir auf beiden Seiten eine halbe Liniendicke hinzuaddieren. Angegebene Breite: 8 p
Angegebene Breite: 8 p
Effektive Breite: 9 p
Effektive Breite: 9 p
Der effektive Zeichenbereich eines Rechteckes vergrößert sich an jeder Kante um die Hälfte der Linienstärke. Hier am Beispiel von Rechteck und Ellipse.
Aus der Existenz einer Linienstärke ergibt sich zudem das Problem, dass die Linienenden eine Form haben müssen. Sie können abgeschnitten sein, abgerundet oder rechteckig. Gleiches gilt für die Übergänge bei Linienzügen: Werden Linien aneinandergefügt, so entsteht eine Aussparung, die geschlossen werden muss. Nur wie? Bei den Bezier-Pfad-Attributen werden wir die verschiedenen Möglichkeiten darstellen.
Durch die Linienstärke entstehen Lücken in Linienzügen.
23
SmartBooks
Objective-C und Cocoa – Band 2
Antialiasing vermeiden Manchmal, insbesondere bei Haarlinien für Gitternetze und dergleichen, möchte man diesen Effekt nicht. Es ist daher möglich, das Antialiasing auszuschalten, wie wir es später auch mal machen werden. Dann müssen die Koordinaten pixelgenau berechnet werden. Wer aus den bisherigen Darstellungen glaubt, dies einfach machen zu können, indem er »halbe« Koordinaten wie { 6.5, 2.5 } verwendet, geht leider fehl. Dies hat zwei Gründe:
•
Grundsätzlich können unsere Koordinatenangaben zum Bildschirm um Teilpixel verschoben, vergrößert oder gedreht sein. Damit werden wir gleich auch arbeiten.
•
Seit Leopard ist es möglich, dass die in unserem Koordinatensystem verwendeten Einheiten nicht mehr den Pixeln entsprechen, was bis Mac OS X 10.4 eine zulässige Annahme war. »Eine Einheit weiter« führt dann also nicht zum nächsten Pixel, sondern irgendwohin dazwischen.
Langer Rede kurzer Sinn: Sogar wenn eine Linie der Breite 1 mit Koordinaten gezeichnet wird, die auf .5 enden – etwa: { 2.5, 3.5 } zu { 2.5, 7.5 }, so bedeutet dass immer noch nicht, dass exakt Pixel bedeckt werden. Vergessen Sie solche Annahmen einfach. Sie sollten damit leben, dass sich das System um die richtige Positionierung und Darstellung Ihrer Inhalte kümmert. Wir besprechen den Zusammenhang zwischen den logischen Angaben in unserem Koordinatensystem mit Einheiten und den physikalischen Pixeln auf dem Bildschirm in einem eigenen Abschnitt am praktischen Beispiel noch genauer. Wir wollen das hier nur erwähnt wissen, damit Sie vielleicht bei dem ein oder anderen Nebeneffekt nicht verwundert die Augen reiben – oder gar zum Optiker rennen, weil Sie die verwischten Linien für ein Problem Ihrer Sehkraft halten.
Bounds- und Frame-Koordinaten Bisher haben wir nur darüber gesprochen, wie wir geometrische Figuren in dem Koordinatensystem platzieren. Zu jedem View gehören jedoch zwei Koordinatensysteme. Das Frame-Koordinatensystem und das Bounds-Koordinatensystem. Das Frame-Koordinatensystem haben wir auch schon benutzt: So hat unser Customview im Contentview des Fensters eine Lage und eine Ausdehnung, die wir im Size-Inspector des Interface Builders betrachten und eingeben konnten. Diese Lage und Ausdehnung bilden ein Rechteck, das sogenannte Frame-Rechteck. Aber jetzt wird es etwas komplizierter: Die angegebenen Koordinaten beziehen sich auf dieses Contentview. Das können Sie leicht erkennen, wenn Sie das Fenster ver24
Kapitel 1
Graphische Ausgabe
schieben: Die Koordinaten ändern sich nicht, weil sich die Lage des Customviews im Contentview nicht ändert. Verschieben Sie jedoch das Customview im Con tentview, so ändern sich die Koordinaten. (Das war jetzt so selbstverständlich, dass Sie vermutlich noch nie darüber nachgedacht haben.) Dieses Frame-Koordinatensystem bezieht sich also immer auf das umgebene Superview, also »nach außen«. Nun kommt der Trick: Wenn wir gleich selbst in diesen Customview zeichnen, dann verwenden wir hierbei ein neues Koordinatensystem, und zwar das BoundsKoordinatensystem des Customviews. Der Nullpunkt dieses Koordinatensystems ist die linke untere Ecke des Customviews – und zwar wiederum unabhängig von der Lage des Customviews im Contentview. Wir zeichnen gleich in der Mitte des Customviews einen Kreis. Da wir den Kreis im Customview zeichnen, müssen wir von dessen Bounds-Koordinatensystem ausgehen. Der Mittelpunkt unseres Views ist daher bei { 100, 100 }, gleichgültig, welche Koordinaten das Customview in seinem Superview hat. Dieses »innere« Koordinatensystem bezeichnet man als Bounds (Boundaries, Grenzen). Im Frame-Koordinatensystem läge der Mittelpunkt unseres Views bei {120, 120}. Das interessiert uns nur nicht, weil wir im View zeichnen und daher das innere Bounds-Koordinatensystem relevant ist. 240 200
120
100
0|0 0|0
100
120
200
240
Für die Zeichnerei ist der Mittelpunkt { 100, 100 }.
Die Frame-Koordinaten des Customviews sind wiederum im Bounds-Koordinatensystem des Superviews ausgedrückt: Das Customview befindet sich ja im Superview.
BEISPIEL Wir werden übrigens gleich noch sehen, dass man mit den verschiedenen Koordinatensystemen nicht nur Verschiebungen erzielen kann, sondern auch Vergrößerungen und sogar Drehungen.
25
SmartBooks
80 75
Objective-C und Cocoa – Band 2
60
Subview 45
15
15
30
0, 0 20
Subsubview
40 65
0, 0
70
20
90 100
0, 0
Schon wieder diese russische Matrjoschka: Auch die Koordinaten sind ineinander gestopft.
Probieren ist Veranschaulichen: Zunächst müssen wir dem Interface Builder sagen, dass es sich bei dem View um eine Instanz unserer Klasse handelt. Dazu wählen Sie bitte den Customview an und wechseln im Inspector auf das Identity-Pane. Dort geben Sie als Klasse unsere Klasse TableBaizeView ein. Hiermit ist alles getan, damit später unser View gezeichnet wird. Zu diesem Zwecke öffnen Sie bitte TableBaizeView.m und ergänzen den Code: - (void)drawRect:(NSRect)rect { NSEraseRect( [self bounds] ); [[NSColor blackColor] set]; NSRect circleRect = NSMakeRect( 50.0, 50.0, 100.0, 100.0 ); NSBezierPath* circle; circle = [NSBezierPath bezierPathWithOvalInRect:circleRect]; [circle stroke]; NSLog( @"Frame: %@", NSStringFromRect( [self frame] ) ); NSLog( @"Bounds: %@", NSStringFromRect( [self bounds] ) ); }
Wir werden uns um das Zeichnen noch im Einzelnen kümmern. Hier nur so viel, dass Sie diesen Code verstehen: Mit NSEraseRect() kann ein weißer Hintergrund gemalt werden. Wir übergeben an diese Funktion unsere Bounds. Da wir bei Programmstart die Bounds-Koordinaten origin = { 0, 0 } und size = { 200, 200 } haben, wird eben unser komplettes View mit einem weißen Hintergrund versehen. Mit dem oben Ausgeführten könnte man auch leicht auf den Gedanken kommen, 26
Kapitel 1
Graphische Ausgabe
lediglich dasjenige Rechteck neu zeichnen zu lassen, das neu gezeichnet werden muss. Der entsprechende Code würde ohne Weiteres funktionieren und sähe so aus: - (void)drawRect:(NSRect)rect { NSEraseRect( rect ); …
In der nächsten Zeile wird Schwarz als Zeichenfarbe gesetzt. Es wird ein Rechteck erzeugt, welches bei { 50, 50 } beginnt und eine Größe von { 100, 100 } hat. Für dieses Rechteck lassen wir uns einen Kreis erzeugen. Dazu benutzen wir die gleich noch zu besprechenden Bezier-Pfade, in welchen sich geometrische Figuren wie eben ein Kreis abspeichern lassen. Schließlich zeichnen wir den Bezier-Pfad mit -stroke. Wir haben jetzt also einen Kreis, dessen Mittelpunkt bei { 100, 100 } liegt und der einen Radius von 50 hat. 240
100
r=50
70
100 50, 50
0, 0
70
240
Der Kreis befindet sich in einem Rechteck der Größe 100 x 100 und hat daher einen Radius von 50.
Starten Sie bitte das Programm und schauen Sie sich das Ergebnis an. Sie werden einen entsprechend zentrierten Kreis vorfinden. Schauen wir auch in den Log: >… Frame: {{20, 20}, {200, 200}} >… Bounds: {{0, 0}, {200, 200}}
Sie sehen beim Frame das Rechteck, welches die Lage und Ausdehnung des Views im Contentview beschreibt. Bei den Bounds sehen Sie das für unsere Zeichnerei zulässige Rechteck. Man kann also sagen, dass das Bounds-Rechteck auf das FrameRechteck projiziert wird: Wir zeichnen nur mit den Bounds-Koordinaten, und das System sorgt dafür, dass dies auf die richtige Stelle im Contentview umgerechnet wird. Dies geht übrigens durch beliebig viele Ebenen: Auch das Contentview liegt 27
SmartBooks
Objective-C und Cocoa – Band 2
ja in einem Superview, dem »geheimen Hintergrundview« (Band I, S. 338f.). Und dieses wiederum hat einen Ort auf dem Bildschirm. Wechseln Sie bitte noch einmal in den Interface Builder und verschieben Sie das Customview so, dass es die rechte, untere Kante des Contentviews berührt. Sie sehen jetzt schon im Interface Builder, dass sich das Frame-Rechteck ändert. Starten Sie das Programm und beachten Sie wieder den Log: >… Frame: {{40, 0}, {200, 200}} >… Bounds: {{0, 0}, {200, 200}}
Sie sehen, dass sich das Frame-Rechteck verändert hat, weil es eine andere Lage hat. Unser Bounds-Rechteck ist davon unbeeinflusst, so dass weiterhin in der Mitte des Views ein Kreis erscheint. Wir merken uns also folgende Regeln:
•
Die Lage und Größe eines Views in seinem Superview wird durch das FrameRechteck bestimmt.
•
Alle Ortsangaben innerhalb des Views beziehen sich auf sein Bounds-Rechteck. Nur dieses interessiert uns für unsere Zeichnerei.
•
Das Bounds-Rechteck hat standardmäßig seinen Nullpunkt an seiner linken, unteren Ecke. Die Größe des Bounds-Rechteckes entspricht standardmäßig der Größe des Frame-Rechteckes (hier: 200 x 200).
•
Das System projiziert automatisch das Bounds-Rechteck auf das Frame-Rechteck.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 1« von der Webseite herunterladen.
28
Kapitel 1
Graphische Ausgabe
Änderung des Bounds-Rechteckes Cocoa und OS X erleichtern uns also die Zeichenarbeit, indem sie uns immer in denselben Rechtecken zeichnen lassen, gleichgültig, wo sich das Rechteck befindet. Aber es geht noch weiter: Wir können auch unser Bezugssystem, also das BoundsRechteck verändern. Verschiebungen (Translation) Um das zu testen, ändern wir unser Applikationsdelegate: In Card_GameAppDelegate.h kommt ein Outlet auf unser View, damit wir es später aus dem Code heraus ansprechen können: @interface Card_GameAppDelegate : NSObject { IBOutlet NSView* tableBaizeView; … }
In AppDelegate.m schreiben wir eine kurze Routine: @implementation Card_GameAppDelegate - (void)applicationDidFinishLaunching:(NSNotification*)notif { [tableBaizeView setBoundsOrigin:NSMakePoint( -50.0, -50.0 )]; } @end
Origin ist ja die untere linke Ecke unseres Views, die also standardmäßig bei { 0, 0 } lag. Mit dieser Nachricht sorgen wir also dafür, dass die untere linke Ecke nunmehr die Koordinaten { -50, -50 } hat – im Bounds-System. Die Lage unseres Views ändert sich hierdurch also nicht, denn diese wird ja durch das Frame-System bestimmt. Um das Ganze zu testen, müssen wir noch eine Instanz unseres App-Delegates im Interface Builder erzeugen und verbinden: Öffnen Sie wieder MainMenu.xib im Interface Builder und suchen Sie das Applikationsdelegate. Verbinden Sie nun das Outlet des App-Delegates mit dem Table BaizeView. Sie werden nach Übersetzen und Starten des Programmes bemerken, dass der Kreis in die rechte, obere Ecke gewandert ist. Dies liegt daran, dass das System nunmehr aufgrund unserer geänderten Angaben eine neue Projektion vornimmt.
29
SmartBooks
Objective-C und Cocoa – Band 2
200
220|220
100|100
0|0
200
-50, -50
20, 20
Man kann sein privates Koordinatensystem einstellen.
Wir haben also nicht nur den Vorteil, dass wir unabhängig von irgendwelchen Positionierungen zeichnen können. Es ist sogar möglich, selbst zu bestimmen, welches Koordinatensystem benutzt wird.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 2« von der Webseite herunterladen. Vergrößerungen (Skalierung) Die Freiheit, das eigene Zeichenrechteck (Bounds) zu bestimmen, funktioniert nicht nur für die Lage des Nullpunktes, sondern auch für die Größe des Rechteckes. Bitte ändern Sie in AppDelegate.m wie folgt: - (void)applicationDidFinishLaunching:(NSNotification*)notif { [tableBaizeView setBoundsOrigin:NSMakePoint( -50.0, -50.0 )]; [tableBaizeView setBoundsSize:NSMakeSize( 150.0, 150.0 )]; }
Mit dieser Zeile wird also gesagt, dass die Ausdehnung unseres Koordnatensystems nur noch 150 x 150 anstelle von 200 x 200 betragen soll. Wenn Sie das Programm starten, bemerken Sie vielleicht überrascht, dass etwas mehr als das untere, linke Viertel des Kreises sichtbar ist. Und auch in der Konsole erscheint etwas Seltsames: 30
Kapitel 1
Graphische Ausgabe
>… {{40, 0}, {200, 200}} >… {{-37.5, -37.5}, {150, 150}}
150
220
0|0
150
-37.5, -37.5 12.5
20, 20
62.5
220
Windschief: Es erscheint etwas mehr als der erwartete Viertelkreis.
Hatten wir nicht {50, 50} gesetzt? Der Trick besteht darin, dass wir in der zweiten Zeile die Ausdehnung pro Achse von 200 auf 150 verkleinern. Cocoa nimmt dies zum Anlass, auch den Ursprung von ursprünglich { -50, -50 } maßstabsgerecht auf { -37.5, -37.5 } umzurechnen. Möchten wir das nicht, so müssen wir die beiden Zeilen vertauschen: - (void)applicationDidFinishLaunching:(NSNotification*)notif { [tableBaizeView setBoundsSize:NSMakeSize( 150.0, 150.0 )]; [tableBaizeView setBoundsOrigin:NSMakePoint( -50.0, -50.0 )]; }
Es geht aber noch einfacher: - (void)applicationDidFinishLaunching:(NSNotification*)notif { NSRect newBounds = NSMakeRect( -50.0, -50.0, 150.0, 150.0 ); [tableBaizeView setBounds:newBounds]; } @end
31
SmartBooks
Objective-C und Cocoa – Band 2
150
220
0, 0
150
-50, -50
220
20, 20
Rechte Winkel beruhigen das Auge ungemein – und den Programmierer.
TIPP Es gibt keine Methode, um die Komponenten origin und size gesondert zu lesen. Dies ist auch nicht erforderlich, denn die Methode -bounds liefert ein Rechteck zurück, auf dessen Komponenten wir wie bei jeder Struktur mit der Punkt-Notation zugreifen können: [self bounds].origin Aber noch etwas anderes ist zu beachten: Da die Größe unserer Bounds sich jetzt von der Größe unseres Frames unterscheidet, muss Cocoa eine Vergrößerung beim Zeichnen vornehmen. Klarer wird dies beim umgekehrten Fall: - (void)awakeFromNib { NSRect newBounds = NSMakeRect( -50.0, -50.0, 400.0, 400.0 ); [tableBaizeView setBounds:newBounds]; }
Da wir in dem View jetzt ein Rechteck mit der Ausdehnung von 400 x 400 haben, dies aber auf dem Bildschirm in einem Rechteck mit der Ausdehnung 200 x 200 gezeichnet wird, muss die gesamte Zeichnung verkleinert werden.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 3« von der Webseite herunterladen.
32
Kapitel 1
Graphische Ausgabe
AUFGEPASST Sie bemerken vielleicht, dass sich die Farbe unseres Kreises »abgeschwächt« hat: Es ist nur noch grau statt schwarz. Dieser Effekt hängt damit zusammen, dass die Umrandungslinie bei der Verkleinerung in der Umrechnung schmaler als 1 Pixel wird. Damit bedeckt die Linie weniger Fläche der betroffenen Pixel und wird entsprechend heller. Drehung (Rotation) Schließlich ist es möglich, das Boundssystem zu rotieren. Hierzu dient die Methode -setBoundsRotation: (NSView). Es sei bereits hier angemerkt, dass die Rotation des gesamten Koordinatensystems eher schlecht handhabbar ist. Die liegt daran, dass das Bounds-Rechteck stets rechtwinklig zum Schirm bleibt. Der Vorteil, einfach aus unserem Bounds-Rechteck heraus das Layout berechnen zu können, verschwindet dann. Dennoch hier zur Vollständigkeit: Zunächst zeichnen wir noch ein Rechteck im View, um die Rotation besser sehen zu können. Ergänzen Sie in TableBaizeView.m: - (void)drawRect:(NSRect)rect { … [circle stroke]; NSRect boxRect = NSMakeRect( 0.0, 0.0, 100.0, 100.0 ); NSBezierPath* box; box = [NSBezierPath bezierPathWithRect:boxRect]; [box stroke]; NSLog( @"Frame: %@", NSStringFromRect( [self frame] ) ); NSLog( @"Bounds: %@", NSStringFromRect( [self bounds] ) ); }
Die Rotation des Rechteckes wird dann wiederum in AppDelegate.m gesetzt: - (void)applicationDidFinishLaunching:(NSNotification*)notif { NSRect newBounds = NSMakeRect( 0.0, 0.0, 200.0, 200.0 ); [tableBaizeView setBounds:newBounds]; [tableBaizeView setBoundsRotation:10.0]; }
33
SmartBooks
Objective-C und Cocoa – Band 2
0°-, 10°- und 20°-Drehung
Beachten Sie hierbei bitte auch die »krummen« Werte im Log, die sich aus der Rotation ergeben: >… Frame: {{20, 20}, {200, 200}} >… Bounds: {{0, -34.7296}, {231.691, 231.691}}
Bei Drehungen bestimmt sich das BoundsRechteck kartesisch, was zu »krummen« Werten führt.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 4« von der Webseite herunterladen. Sie sollten auch bemerken, dass die Zeichnung automatisch abgeschnitten wird, wenn sie das View verlässt. Dies bezeichnet man als »Clipping«. Es ist daher gefahrlos möglich, den View auch dann zu zeichnen, wenn er nicht vollständig sichtbar ist. 34
Kapitel 1
Graphische Ausgabe
Spiegeln (Flipping) Sie haben jetzt ja in einigen Beispielen gesehen, dass sich die X-Koordinaten nach rechts, die Y-Koordinaten nach oben vergrößern. Das entspricht der mathematischen Sicht der Dinge und ist bei Cocoa Standard. Zuweilen ist das aber unbequem, weil man im Computerbereich die Vergrößerung der Y-Achse nach unten gewöhnt ist. Um diesen Zustand zu erreichen, muss in der Subklasse TableBaizeView eine weitere Methode implementiert werden: @implementation TableBaizeView - (BOOL)isFlipped { return YES; } - (id)initWithFrame:(NSRect)frame {
Wenn Sie das Programm starten, werden Sie sehen, dass das View nunmehr auf dem Kopf steht.
Upside-Down: Wir können das Koordinatensystem klassisch nutzen.
Ich bemerke allerdings, dass durch Flipping die Sache komplizierter wird, weil man gedanklich noch einmal alles spiegeln muss. Ich empfehle daher, sich gleich an das mathematische Koordinatensystem von Cocoa zu gewöhnen.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 5« von der Webseite herunterladen.
35
SmartBooks
Objective-C und Cocoa – Band 2
Änderung des Frame-Rechtecks Wir hatten da ja noch das Autoresizing … Was passiert eigentlich, wenn wir das Fenster vergrößern und dadurch das Frame-Rechteck verändern? Wie wäre es mit Probieren? Ziehen Sie das Fenster auf. Wie Sie in der Darstellung und auch im Log sehen können, wird von Cocoa das Bounds-Rechteck automatisch maßstabsgetreu angepasst. Das führt dazu, dass unsere Zeichnung am Orte des Geschehens stehenbleibt.
Ändert sich das äußere Koordinatensystem (Frame), so wird das innere (Bounds) maßstabsgetreu angepasst.
AUFGEPASST Da wir nunmehr unser View flippen, ist der Ankerpunkt die obere, linke Ecke. Es wäre die untere, wenn wir das Flipping nicht durchführten. Sie können das ja mal ausprobieren. Dies ist in aller Regel das gewünschte Verhalten. Wollen wir jedoch, dass das innere Bounds-Koordinatensystem unverändert bleibt, so müssen wir es entsprechend bei einer Änderung der Frames neu setzen. Dies funktioniert übrigens bei einer Bounds-Rotation nicht mehr. Bedenken Sie angesichts der obigen Darstellung, dass hier die Bounds kartesisch »umgerechnet« wurden und nicht mehr die wahren Bounds darstellen. Also nehmen wir zunächst einmal im Applikationsdelegate die Rotation heraus:
36
Kapitel 1
Graphische Ausgabe
- (void)applicationDidFinishLaunching:(NSNotification*)notif { NSRect newBounds = NSMakeRect( 0.0, 0.0, 200.0, 200.0 ); [tableBaizeView setBounds:newBounds]; }
Als Nächstes überschreiben wir in TableBaizeView.m die Methode, die die neue Frame-Größe setzt: - (void)setFrameSize:(NSSize)frameSize { NSRect bounds = [self bounds]; [super setFrameSize:frameSize]; [self setBounds:bounds]; } - (void)drawRect:(NSRect)rect {
Sie können jetzt sehen, dass sich die Zeichnung automatisch in der Größe anpasst, wenn sich das Frame-Rechteck vergrößert. Bitte entfernen Sie wieder -setFrameSize: in TableBaizeView.m sowie das Drehen des Frames in AppDelegate.m.
GRUNDLAGEN Wir werden später noch die sogenannten affinen Transformationen besprechen. Alle hier genannten Beziehungen zwischen Bounds- und FrameRechteck (Verschiebung, Vergrößerung, Drehung) lassen sich durch diese darstellen.
Scrolling Vielleicht ahnen Sie es schon: Wenn ich mit Frames und Bounds Darstellungen verschieben kann, lässt sich auch das Scrolling so implementieren. Ja, genau so macht es Cocoa. Um das mal kurz zu testen, gehen Sie bitte wieder in den Interface Builder und wählen das TableBaizeView an. Im Menü benutzen Sie die bereits aus Band 1 bekannte Funktion Layout | Embed Objects In | Scroll View. Setzen Sie im Size-Inspector die Größe für das Scrollview wieder auf 200 x 200. Außerdem setzen Sie alle Optionen für das Autoresizing, damit sich auch unser Scrollview mit dem Fenster vergrößert.
37
SmartBooks
Objective-C und Cocoa – Band 2
TIPP Wenn Sie die Bezeichnungen und Struktur der verschiedenen Elemente eines Scrollviews nicht mehr im Kopf haben, können Sie diese in Band 1 auf S. 403 ff. nachlesen. Nunmehr klicken Sie sich im Scrollview durch, bis in der Titelzeile des Inspectors wieder TableBaizeView erscheint. Setzen Sie die Größe des TableBaizeViews auf 400 x 400. Sie werden erkennen, dass im Scrollview Slider hinzugefügt werden.
TIPP Man kann jetzt nicht mehr die Klassenbezeichnung in der Mitte des TableBaizeViews sehen. Sie können sich aber zu den Slidern durchklicken und diese auch im Interface Builder bedienen.
150
86, 150
86 0, 0
Beim Scrolling werden die Bounds-Koordinaten des Clipviews verborgen.
38
Kapitel 1
Graphische Ausgabe
Das Scrolling funktioniert, indem der TableBaizeView verschoben wird. Dabei wird aber nicht das Frame-Rechteck des Views angerührt Das TableBaizeView ist vielmehr als Documentview in einem Clipview enthalten. Das bedeutet, dass das Frame-Rechteck des TableBaizeViews sich im Clipview befindet, sich also auf dessen Bounds-Rechteck bezieht. In der obigen Abbildung ist etwa im inneren Bounds-Rechteck des Clipviews der Ursprung auf { 86, 150 } gesetzt. Da sich unser TableBaizeView als Documentview des Scrollviews in dem Clipview befindet, befindet sich sein Ursprung mit { 0, 0 } links unterhalb der sichtbaren Fläche. Wir lassen uns das mal ausgeben: - (void)drawRect:(NSRect)rect { … NSLog( @"Frame: %@", NSStringFromRect( [[self superview] frame] ) ); NSLog( @"Bounds: %@", NSStringFromRect( [[self superview] bounds] ) ); }
Schauen Sie in den Log: Das Bounds-Rechteck des Clipviews ändert sich. Bleibt die Frage, warum das so ist. Immerhin wäre es naheliegend, dass man einfach das Frame-Rechteck des TableBaizeViews verändert. Bedenken Sie aber, dass das TableBaizeView uns als Anwendungsprogrammierer gehört. Das Clipview ist indessen Bestandteil der von Cocoa mit dem Scrollview erzeugten Instanzen. Auf diese Weise erfolgt also eine bessere Trennung zwischen Framework und Anwendung.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 6« von der Webseite herunterladen.
Geometrische Funktionen Sie haben schon bemerkt, dass wir es häufig mit Rechtecken zu tun haben, die als C-Struktur vom Typen NSRect modelliert sind. Diesen kennen Sie bereits aus Band I (S. 138 f.): Die Komponenten dieser Struktur sind wieder Strukturen des Typs NSPoint und NSSize, die wiederum jeweils zwei Zahlen des Typs CGFloat
39
SmartBooks
Objective-C und Cocoa – Band 2
enthalten. Und so kompliziert, wie das hier klingt, ist das häufig auch in der Anwendung. Errechnen wir mal den Mittelpunkt eines Rechteckes: NSPoint center; center.x = rect.origin.x + rect.size.width / 2.0; center.y = rect.origin.y + rect.size.height / 2.0;
Aus diesem Grunde bietet Cocoa eine ganze Fülle von Funktionen, die das Arbeiten insbesondere mit Rechtecken erleichtern. Es sei hier jedoch erwähnt, dass manche dieser Funktionen Probleme haben oder ein abweichendes Verhalten aufweisen, wenn die Breite oder die Höhe eines Rechteckes negativ sind. Hierzu konsultieren Sie bitte die Referenz-Dokumentation. Erzeugungsfunktionen Es existieren Funktionen zum Erzeugen der beteiligten Strukturen, die das Ergebnis als Rückgabewert liefern. Funktion
Beschreibung
NSMakePoint( x, y )
Erzeugt einen Punkt mit { x, y }
NSMakeSize( w, h )
Erzeugt eine Größe mit { w, h }
NSMakeRect( x, y, w, h )
Erzeugt ein Rechteck { x, y, w, h }
Es existieren zudem die Konstanten NSZeroPoint, NSZeroSize und NSZeroRect, bei denen alle Komponenten auf 0 stehen. Zugriffsfunktionen Ähnliches erlebt man, wenn man andere Kanten oder Eckpunkte berechnen will. Aus diesem Grunde existieren Funktionen, die die Arbeit erleichtern. Sie erhalten als Parameter stets ein Rechteck. Funktion
Beschreibung
NSMinX( rect )
X-Koordinate der linken Kante
NSMaxX( rect )
X-Koordinate der rechten Kante
NSMidX( rect )
X-Koordinate des Mittelpunktes
NSMinY( rect )
X-Koordinate der linken Kante
NSMaxY( rect )
X-Koordinate der rechten Kante
NSMidY( rect )
X-Koordinate des Mittelpunktes
NSWidth( rect )
Breite
NSHeight( rect )
Höhe
40
Kapitel 1
Graphische Ausgabe
NSWidth() NSMaxY()
NSMidY()
NSHeight()
NSMinY()
NSMinX()
NSMidX()
NSMaxX()
Bequemer als endloses Getippe
NSInsetRect() Mit NSInsetRect wird ein neues Rechteck erzeugt, welches ein bestehendes Rechteck um die beiden Offsets dX und dY an jeder Kante nach innen setzt. Das resultierende Rechteck ist also um { dX, dY } versetzt und um { 2dX, 2dY } verkleinert. Auf diese Weise können wir eine dicke Umrandung vollständig in einem View platzieren. (Denken Sie daran, dass die Linienbreite die Ausdehnung vergrößert!) - (void)drawRect:(NSRect)rect { NSFrameRectWithWidth( [self bounds], 4.0 ); NSLog( @"%@", NSStringFromRect( [self bounds] ) ); NSRect insetRect = NSInsetRect( [self bounds], 2.0, 2.0 ); NSLog( @"%@", NSStringFromRect( insetRect ) ); [[NSColor greenColor] setStroke]; [NSBezierPath setDefaultLineWidth:4.0]; [NSBezierPath strokeRect:insetRect]; }
41
SmartBooks
Objective-C und Cocoa – Band 2
dY
dX
dX
dY
Der Offset gilt für jede Seite gesondert.
Es können negative Werte für die Offsets übergeben werden, so dass das resultierende Rechteck größer als das übergebene ist. NSOffsetRect() … liefert als Rückgabewert ein Rechteck, welches um die Parameter dX und dY verschoben ist. NSDivideRect() Diese Funktion teilt ein Rechteck in zwei Teile, genannt »slice« (Scheibe) und »remaining« (Rest). Durch wiederholtes Anwenden lassen sich damit Rechtecke in Regionen unterteilen. Als Parameter erhält die Funktion unter anderem rectEdge, mit dem die Richtung der Unterteilung angegeben wird. Die entsprechenden Konstanten entnehmen Sie bitte der Abbildung.
remaining
slice
NSMinYEdge
amount
NSMaxXEdge
NSMinXEdge
NSMaxYEdge
Ausgehend von einer Kante wird eine Scheibe abgeschnitten.
42
Kapitel 1
Graphische Ausgabe
NSUnionRect() Wie Sie bereits gelernt haben, bildet der Parameter rect von -drawRect: das umfassende Rechteck der einzelnen Rechtecke, die zum Neuzeichnen markiert wurden. Eine solche Rechteck-Vereinigung nimmt NSUnionRect() vor. Es liefert dasjenige Rechteck, welches gerade die beiden übergebenen Rechtecke enthält. NSIntersectionRect() … stellt sozusagen das Gegenstück dar. Es ermittelt ein gemeinsames Rechteck von zwei Rechtecken. Überlappen sich beide Rechtecke nicht, so ist das Ergebnis NSZeroRect.
AUFGEPASST Dies gilt auch dann, wenn sich die Rechtecke lediglich berühren. Es wird dann also nicht die Schnittkante bzw. der Schnittpunkt mit entsprechender Größe 0 zurückgeliefert. NSIntegralRect() NSIntegralRect rundet die Komponenten so, dass alle Kanten (und damit auch die Größenangaben) glatte Werte haben, also keine Nachkommastellen. Dabei wird das Rechteck vergrößert, also die linke bzw. untere Kante abgerundet, die rechte bzw. obere Kante aufgerundet. NSEqualRect(), NSEqualPoint() und NSEqualSize() Liefert jeweils den boolschen Wert YES, wenn die übergebenen Parameter gleich sind, andernfalls NO. Beachten Sie aber, dass dabei notgedrungen Fließkommazahlen verglichen werden müssen, was ob der Rundungsfehler eine heikle Angelegenheit ist. NSContainsRect() und NSPointInRect() Mit diesen beiden Funktionen können Sie überprüfen, ob ein Rechteck vollständig bzw. ein Punkt in einem Rechteck liegt. Dies ist nützlich für die oben angesprochenen Optimierung des Zeichnens: So könnte man die Rechtecke der einzelnen Symbole daraufhin überprüfen, ob sie im rect-Parameter von -drawRect: liegen. Ist dies nicht der Fall, so müssen sie nicht gezeichnet werden. PointInRect() wird dagegen – später auch von uns – dazu verwendet, um zu überprüfen, ob ein Punkt (etwa der Ort eines Mausklicks) in einem Rechteck liegt.
HILFE Sie können sich ein entsprechendes Lesezeichen von der Webseite herunterladen, auf dem die wichtigsten Funktionen dargestellt sind.
43
SmartBooks
Objective-C und Cocoa – Band 2
Zeichnen Bitte leeren Sie -applicationDidFinishLaunching: im Application-Delegate, -drawRect: in TableBaizeView.m und entfernen Sie dort -isFlipped. Befreien Sie im Interface Builder den TableBaizeView mit Layout | Unembed Objects wieder aus seinem Scrollview-Gefängnis. Ziehen Sie das Fenster deutlich größer und platzieren Sie den TableBaizeView dort so, dass er das Fenster komplett belegt, jedoch am unteren Ende einen Rand lässt. Überprüfen Sie bitte, dass die Autoresizing-Eigenschaften für den View sämtlich gesetzt sind.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 7« von der Webseite herunterladen. Nachdem wir viel über das Wie und Wo gesprochen haben, sollten wir uns nun die Möglichkeiten des Zeichnens anschauen. Grundsätzlich existieren mehrere. Von Cocoa aus betrachtet sind die wichtigsten
•
C-Funktionen für einfache Operationen. Wir hatten hier schon NSEraseRect() benutzt.
• •
Gradients für Farbverläufe
•
Bezier-Pfade für komplexere Gebilde. Auch diese hatten wir bereits für den Kreis und das Rechteck verwendet. Images für vorgefertigte Graphiken
Diese verschiedenen Möglichkeiten stehen nicht nebeneinander, sondern bedingen sich gegenseitig. So kann ein Farbverlauf selbst gezeichnet werden, aber auch Füllfarbe für einen Bezier-Pfad sein. Die Zeichnerei hängt zudem an Attributen wie Farbe, Linienstärke, Linienenden usw. Auch hier hatten wir schon ein – und zwar das wichtigste Beispiel: -set (NSColor), welches die Linien- und Füllfarbe setzt. Mit - setStroke und -setFill lassen sich die Farben unabhängig setzen.
44
Kapitel 1
Graphische Ausgabe
GRUNDLAGEN Unter der Cocoa-Welt hängen OS-X-Technologien, die das eigentliche Zeichnen übernehmen. Zu nennen sei hier vor allem Core Graphics. Es handelt sich dabei um ein C-Framework, welches vor allem auf dem iPhone eingesetzt wird. Die dort anzutreffenden Ideen, Technologien und Konzepte gleichen allerdings den Ansätzen von Cocoa. Cocoa ist gewissermaßen eine Schale um Core Graphics, so dass es inhaltlich nicht viel Neues gibt. Am Ende gehen wir aber kurz auf die Möglichkeit ein, Core Graphics zu nutzen. Eine weitere Technologie ist OpenGL (iPhone: OpenGL ES), welche vor allem bei stark graphiklastigen Anwendungen wie Spielen eingesetzt wird. Speziell hierfür existiert aus der Reihe im Smartbooks Verlag ein Buch von Wolfgang Reminder mit dem Titel »Spieleprogrammierung mit Objective-C/ OpenGL«.
Einfache Zeichenfunktionen Sie hatten bereits eine einfache Zeichenfunktion kennengelernt. Die wichtigsten – und vor allem noch nicht überholten – sind: NSEraseRect() Zeichnet ein Rechteck, welches mit weißer Farbe gefüllt ist. Die aktuellen Farbeinstellungen von -set, -setFill und -setStroke (alle NSColor) werden nicht beeinflusst. NSRectFill(), NSRectFillList() et al. Füllt ein Rechteck mit der aktuell eingestellten Füllfarbe. NSDrawWindowBackground() Zeichnet ein Rechteck im Stile des Fenster-Hintergrundes. NSFrameRect(), NSFrameRectWithWidth() et al. Zeichnet einen Rahmen in der aktuell eingestellten – nein, Vorsicht Falle: nicht Linienfarbe, sondern – Füllfarbe. Die Linie des Rechteckes liegt vollständig innerhalb des Rechteckes, so dass ein etwaig um den Zeichenbereich eingeschaltetes Clipping das Zeichnen nicht behindert.
45
SmartBooks
Objective-C und Cocoa – Band 2
Angegebene Breite: 8 p
Clip-Bereich
Linie wächst nach innen NSFrameRect() verkleinert das Rechteck so, dass die Umrandungslinien komplett im angegebenen Rechteck liegen.
Wir können die einmal nachprüfen. Fügen Sie in TableBaizeView.m in -drawRect: folgenden Code ein: - (void)drawRect:(NSRect)rect { NSFrameRectWithWidth( [self bounds], 4.0 ); }
Mit -bounds geben wir ja den kompletten Zeichenbereich an. Dementsprechend müssten sämtliche 4 Einheiten – hier nehmen wir zur Vereinfachung Pixel an – innerhalb des Views liegen. Schauen wir uns das mit dem vorerwähnten Programm Pixie an: Sie sehen die Umrandung mit vier Pixeln. Zeichnen wir indessen mit Bezier-Pfaden, also ohne die Sonderfunktionalität von NSFrameRect(), so verschwindet die Hälfte der Begrenzungslinie: - (void)drawRect:(NSRect)rect { NSFrameRectWithWidth( [self bounds], 4.0 ); [[NSColor greenColor] setStroke]; [NSBezierPath setDefaultLineWidth:4.0]; [NSBezierPath strokeRect:[self bounds]]; }
Wie Sie mit Pixie sehen können, wird die schwarze Linie von der grünen Linie nur zwei Pixel weit überdeckt. Der Rest der grünen Linie liegt außerhalb des Views und wird daher nicht gezeichnet (Clipping).
46
Kapitel 1
Graphische Ausgabe
Angegebene Breite: 8 p
Clip-Bereich Ungezeichneter Bereich
Linie wächst beidseitig »Üblicherweise« wächst die Linie in beiden Richtungen.
NSDrawThreePartImage(), NSDrawNinePartImage() NSDrawThreePartImage() zeichnet nebeneinander oder untereinander drei Bilder, wobei das erste Bild (Start-Caption) den einen Rand, das letzte Bild den anderen Rand darstellt (End-Caption) und der Zwischenbereich mit dem Mittelbild ausgefüllt wird. Dabei wird das Mittelbild gegebenenfalls so häufig wiederholt, bis der Zeichenbereich ausgefüllt ist. Diese Operation kann sowohl vertikal als auch horizontal ausgerichtet werden. In der unflexiblen Richtung (in der Abbildung die YRichtung) sollten alle Bilder dieselbe Größe (in der Abbildung: Höhe) haben.
Start-Caption
Fill-Caption
End-Caption
Für eigene Controls praktisch: Der Mittelteil wird in ausreichender Weise wiederholt.
NSDrawNinePartImage() ist das Pendant für sich in beide Richtung ausdehnende Controls. Auch hier muss in der jeweiligen Richtung die Einhaltung der Größe beachtet werden.
47
SmartBooks
Objective-C und Cocoa – Band 2
Ränder, Kanten und Innereien: Es lassen sich auch zweidimensional skalierbare Controls zeichnen.
NSDrawButton(), NSDrawDarkBezel(), NSDrawLightBezel(), NSDrawGrayBezel(), NSDrawWhiteBezel() NSDrawGroove() NSDrawTiledRects(), NSDrawColorTiledRects() Zeichnet Rechtecke mit verschiedenen Kantenfarben, wobei die letzten Funktionen mehrere Kanten ineinander setzen können. Für Cocoa sollte man eher zu NSButton greifen.
Gradients Gradients sind Farbverläufe. Sie sind nicht einfach als ein spezieller Fall der Farbe implementiert, sondern als eigene Subklasse NSGradient von NSObject. Sie werden daher auch nicht als Farbparameter für eine andere Zeichenoperation gesetzt, sondern selbst gezeichnet. Häufig verwendet man diese, um Farbflächen etwas ansprechender zu gestalten. So auch wir. Die Instanzen der Klasse NSGradient speichern letztlich nur die verschiedenen beteiligten Farben und ihre Positionen. Man kann grundsätzlich zwei Arten unterscheiden:
•
Bei einem linearen Verlauf werden die verschiedenen Farben entlang einer Achse angeordnet und gezeichnet. Die Achse beginnt standardmäßig unten links – also bei { 0, 0 }, kann aber mit einem Winkel versehen werden.
•
Bei einem radialen Verlauf werden die verschiedenen Farben als Ringe von konzentrischen Kreisen angeordnet. Der Mittelpunkt dieser Kreise kann festgelegt werden. 48
Kapitel 1
Graphische Ausgabe
Allerdings ist es wichtig zu verstehen, dass eine Instanz von NSGradient selbst nicht als linearer oder radialer Verlauf bezeichnet werden kann. Sie speichert nur die Farben und ihre Position entlang der Achse bzw. ihre Entfernung vom Mittelpunkt. Erst bei der Ausgabe selbst wird ein Achsenwinkel (linearer Verlauf) oder ein Mittelpunkt (radialer Verlauf) angegeben.
Ein linearer und ein radialer Gradient in einem Rechteck gezeichnet
Um uns mal ein bisschen aufzuwärmen, zeichnen wir einen Farbverlauf in unserem View: - (void)drawRect:(NSRect)rect NSColor* color1 = [NSColor NSColor* color2 = [NSColor NSColor* color3 = [NSColor
{ magentaColor]; greenColor]; blueColor];
NSArray* colors = [NSArray arrayWithObjects:color1, color2, color3, nil]; NSGradient* gradient = [[[NSGradient alloc] initWithColors:colors] autorelease]; [gradient drawInRect:[self bounds] angle:30.0]; }
Starten Sie das Programm. Sie sehen einen linearen Verlauf, beginnend unten links mit einem 30°-Winkel. Wenn Sie beim Anblick der ganz speziellen Farbkombination nicht Augenkrebs bekommen haben sollten, können wir jetzt weitermachen: Da der Gradient nicht wissen kann, in welchem Rechteck er später gezeichnet wird, werden relative Positionsangaben verwendet, die von 0 (erste Farbe) bis 1 (letzte Farbe) reichen. Da wir keine Angabe gemacht hatten, platziert NSGradient die mittlere Farbe automatisch bei 0.5. Dies ändern wir jetzt einmal: - (void)drawRect:(NSRect)rect NSColor* color1 = [NSColor NSColor* color2 = [NSColor NSColor* color3 = [NSColor
{ magentaColor]; greenColor]; blueColor];
49
SmartBooks
Objective-C und Cocoa – Band 2
NSGradient* gradient = [[[NSGradient alloc] initWithColorsAndLocations:color1, 0.0, color2, 0.2, color3, 1.0, nil] autorelease]; [gradient drawInRect:[self bounds] angle:30.0]; }
Sie sehen, dass das Grün deutlich an den Ursprung unten links gerückt ist.
AUFGEPASST Die Positionen für Farben sollten übrigens einmal 0 (Startfarbe) und einmal 1 (Endfarbe) enthalten. Ist dies nicht der Fall, werden die Enden in den Farben gezeichnet, die am nächsten 0 bzw. 1 kommen. Wollen wir den Gradient als radialen Verlauf zeichnen, funktioniert dies entsprechend: - (void)drawRect:(NSRect)rect NSColor* color1 = [NSColor NSColor* color2 = [NSColor NSColor* color3 = [NSColor
{ magentaColor]; greenColor]; blueColor];
NSGradient* gradient = [[[NSGradient alloc] initWithColorsAndLocations:color1, 0.0, color2, 0.2, color3, 1.0, nil] autorelease]; [gradient drawInRect:[self bounds] relativeCenterPosition:NSMakePoint (0.3, 0.1 )]; }
Sie sehen, dass sich lediglich die letzte Zeile geändert hat, die Instanz von NSGradient jedoch nicht: Wie gesagt, unterscheiden sich die beiden Arten der Gradients nicht durch ihren Inhalt, sondern durch die Art, wie sie in der letzten Anweisung gezeichnet werden. Die Position des Mittelpunktes bezieht sich übrigens auf das Rechteck mit dem Wertebereich -1 bis +1. 50
Kapitel 1
Graphische Ausgabe
Kommen wir aber zu unserer eigentlichen Aufgabe zurück und fügen einen ansehnlichen Gradient hinzu: - (void)drawRect:(NSRect)rect { NSColor* lighterColor = [NSColor colorWithCalibratedRed:0.2 green:0.7 blue:0.2 alpha:1.0]; NSColor* darkerColor = [NSColor colorWithCalibratedRed:0.0 green:0.5 blue:0.0 alpha:1.0]; NSGradient* gradient; gradient = [[[NSGradient alloc] initWithStartingColor:lighterColor endingColor:darkerColor] autorelease]; [gradient drawInRect:[self bounds] relativeCenterPosition:NSMakePoint( -0.5, +0.5 )]; }
HILFE Sie können das Projekt in diesem Zustand als »Card Game 8« von der Webseite herunterladen.
51
SmartBooks
Objective-C und Cocoa – Band 2
Bezier-Pfade Die klassische Methode der graphischen Ausgabe unter Cocoa stellen Bezier-Pfade dar. Bei Bezier-Pfaden handelt es sich um komplexe Zeichnungen, die aus einem oder mehreren Subpfaden bestehen können. Als Subpfade werden angeboten:
• • •
Rechtecke Ellipsen (und damit auch Kreise, wenn X-Radius und Y-Radius gleich sind) freie Subpfade
Bei freien Subpfaden handelt es sich um in sich geschlossene Figuren, die vom Programmierer zusammengesetzt werden. Hierzu bedient er sich verschiedener Segmente, die er zu einer geschlossenen Figur aneinanderreiht. Die Segmente sind:
• • • •
Gerade Linien (Strecken) Kreisbögen Bezierkurven Schriftzeichen in ihrer graphischen Repräsentation (Glyphen) Linie
Kreisbogen
A
Subpfad Subpfad
Bezierkurve
Subpfad Bezierpfad
Pfade bestehen aus Subpfaden, Subpfade aus Segmenten.
Die einzelnen Segmente eines Subpfades sind stets aneinander gereiht. Man bezeichnet dies als »Turtle-Graphik«: Eine Schildkröte mit einem festgebundenen Stift wird an einen Startpunkt auf einer Leinwand gesetzt. Dann erhält sie den Befehl, zu einem anderen Punkt zu laufen, wobei sie das eben in einer gerade Linie machen kann, in einem Kreisbogen oder in einer Bezierkurve. Der Zielpunkt ist dann der Startpunkt für das nächste Segment. Von dort aus geht es also wieder auf die Reise …
52
Kapitel 1
Graphische Ausgabe
Setzen wir explizit einen neuen Startpunkt, so nehmen wir diese Schildkröte kurz hoch und setzen sie an einer anderen Stelle ab. Hierdurch entsteht ein neuer Subpfad. Gleiches gilt, wenn wir einen der vorgefertigten Subpfade (Rechteck, Ellipse, Glyph) dem Bezier-Pfad hinzufügen. Hier erfolgt das Hochnehmen und neue Absetzen der Schildkröte implizit durch NSBezierPath.
Jeweils zwei Subpfade: In der Mitte wurde ein Subpfad nicht geschlossen. Geben wir die Figur ausgefüllt aus, so wird jeder Subpfad automatisch geschlossen (rechts).
Die Apple-Dokumentation spricht an einigen Stellen davon, dass ein offener Subpfad automatisch geschlossen wird, wenn wir einen neuen Subpfad eröffnen. Sei es, weil wir selbst einen neuen Startpunkt setzen, sei es, weil wir einen vorgefertigten Subpfad hinzufügen, also immer dann, wenn die Schildkröte hochgenommen und neu abgesetzt wird. Um es gleich zu sagen: Das ist so nicht richtig! Es existieren nämlich zwei Methoden, die in der Instanz von NSBezierPath beschriebene Figur auszugeben: Man lässt sich den abgespeicherten Linienzug zeichnen oder man lässt die durch den Linienzug beschriebene Fläche ausfüllen. In diesem zweiten Fall ist es freilich erforderlich, dass jeder Subpfad geschlossen ist. Dann – und nur dann – wird auch ein automatisches Schließen der Subpfade von Cocoa vorgenommen, ansonsten nicht! Ein Subpfad ist geschlossen, wenn sein letzter Zielpunkt gleich dem Startpunkt ist. Wir werden gleich die Methode -closePath dazu verwenden, die das erledigt, indem sie eine Linie von der letzten aktuellen Position zur ursprünglichen Startposition zieht. Natürlich ist eine Figur auch geschlossen, wenn die letzte Schildkrötenreise an dem Startpunkt endet. Sie muss dann aber auch genau am Startpunkt enden und nicht nur ungefähr.
53
SmartBooks
Objective-C und Cocoa – Band 2
GRUNDLAGEN Wieso betonen wir das so? Sie haben ja nun gelernt, dass die Koordinaten durch Fließkommazahlen repräsentiert werden. Und bereits im ersten Band wies ich darauf hin, dass Fließkommazahlen eigentlich schrecklich ungenau sind. Wenn Sie also den Pfad von Punkt zu Punkt berechnen, können Sie sich nicht sicher sein, wieder genau am Ursprungsort zu landen. Stellen Sie sich selbst mal in den Raum und machen Sie vier Schritte in einem gedachten Quadrat. Ihre Endposition ist nicht exakt Ihre Ausgangsposition. Wenn Sie also selbst einen Pfad schließen wollen, dann sollten Sie sich den Anfangspunkt merken und die letzte Schildkrötenreise sollte als Zielpunkt diesen Anfangspunkt haben. (-closePath macht auch nichts anderes …)
Bezier-Kurven Was Linien und Kreisbögen sind, müssen wir nicht erläutern. Bezier-Kurven kennt man allerdings gemeinhin nicht. Es sind vereinfacht gesagt frei gekrümmte Linien. Jede Bezierkurve hat daher wie jede Linie zwei Endpunkte, die sie berührt. Dazu treten jedoch zwei Kontrollpunkte, die den Verlauf der Linie bestimmen: Stellen Sie sich ein Brett vor, auf dem Sie mit zwei Nägeln einen flexiblen Holzstab festnageln. Das ist eine gerade Linie, mathematisch eine Strecke. Die Nägel markieren die Endpunkte. Nun schlagen Sie zwei weitere Nägel in das Brett und ziehen von dort mit Gummibändern an dem Holzstab. Er verformt sich und es entsteht eine Kurve, die zwar die Endpunkte berührt, jedoch nicht die weiteren Nägel. So in etwa verhält sich eine Bezierkurve.
GRUNDLAGEN Mathematisch können Bezierkurven beliebig viele Kontrollpunkte enthalten. Sind es wie bei den Bezierkurven von Cocoa zwei, spricht man von »kubischen« Bezierkurven. Es gibt Formeln, um verschiedene Grade von Bezierkurven in andere umzurechnen wie auch andere Modellierungen in BezierPfade umzuwandeln. Das ist dann allerdings langsam höhere Mathematik. Eine gute Einführung liefert die Wikipedia-Seite zur Bezierkurve – übrigens mit zwei wunderbaren Animationen.
Bezier-Pfade haben elastische Beine.
54
Kapitel 1
Graphische Ausgabe
Bezierkurven erfreuen sich in der graphischen Datenverarbeitung großer Beliebtheit. Das liegt daran, dass sie mittels des de-Casteljau-Algorithmusses sehr schnell gezeichnet werden können. Außerdem kann man andere mathematische Modellierungen für Kurven, insbesondere B-Splines, die bei Schriften verwendet werden, in Bezierkurven umrechnen.
GRUNDLAGEN Der Algorithmus nach de Casteljau erzeugt nur eine Näherung der Kurve durch Stückelung in Strecken. Je mehr Strecken verwendet werden, desto besser ist das Ergebnis. Die Klasse NSBezierPath erlaubt es, die Güte (Qualitätsfaktor) mit den Methoden -setFlatness: und -setDefaultFlatness: einzustellen, wobei ein kleinerer Wert eine bessere Qualität bedeutet. Der Standardwert 0,6 von Cocoa sorgt allerdings bereits für ansehnliche Ergebnisse. Höhere Werte beschleunigen die Ausgabe, lassen allerdings die Pfade zunehmend eckig erscheinen. Es kann auch ein Pfad vorab umgewandelt werden, wozu die Methode - bezierPathByFlatteningPath dient. Ihre wichtigste Eigenschaft:
•
Globalität: Jeder Stützpunkt wirkt sich auf die gesamte Kurve aus. Dies ist manchmal etwas ärgerlich, wenn nur Teile verändert werden sollen. Allerdings kann man das Problem umgehen, indem man sie stückelt.
Jeder Kontrollpunkt beeinflusst den gesamten Kurvenverlauf.
•
Interpolation, Approximation: Die Endpunkte werden auf jeden Fall berührt, die Stützpunkte dann, wenn beide auf einer Linie zwischen den Endpunkten liegen, ansonsten angenährt (approximiert).
•
Tangentialität: Die Linie zwischen einem Endpunkt und seinen benachbarten Stützpunkt ist eine Tagente der Kurve.
Die Strecke zum ersten Kontrollpunkt streichelt den Pfad.
55
SmartBooks
•
Objective-C und Cocoa – Band 2
Stetige Verkettbarkeit: Die zwei vorstehenden Eigenschaften können dafür verwendet werden, mehrere Bezierkurven so aneinander zu ketten, dass die Übergänge »glatt« erscheinen. Es sei erneut darauf aufmerksam gemacht, dass die Endpunkte wiederum interpoliert (»getroffen«) werden, so dass man mit dieser Technik einen Bezier-Pfad auch durch eine Vielzahl von festen Stützstellen lotsen kann.
Liegen die benachbarten Kontrollpunkte zweier Kurven auf einer Geraden mit der Nahtstelle, erscheint der Übergang flüssig.
•
Affine Invarianz: Sie hatten je bereits gelernt, dass die Zeichnerei in einem View eine ganze Kette von Transformationen (Vergrößerungen, Verschiebungen und Drehungen) durchläuft, bis letztlich Pixelkoordinaten errechnet sind. Das würde aber bei einer Kurve eine Vielzahl von Umrechnungen bedeuten, weil jeder Punkt auf der Kurve diese Umrechnungen durchlaufen müsste, wenn nicht … Richtig, wenn nicht die Eigenschaft der affinen Invarianz vorläge: Sie bedeutet, dass es gleichgültig ist, ob man die Bezierkurve zunächst zeichnet und dann die oben genannten Transformationen auf jeden einzelnen Punkt der Kurve anwendet oder nur die End- und Kontrollpunkte umrechnet und danach die Kurve zeichnet: Das Ergebnis ist jedes Mal dasselbe. Daher reicht es aus, die Transformationen auf die insgesamt vier Stützpunkte durchzuführen. Das beschleunigt die Sache ungemein.
GRUNDLAGEN Hierdurch wird auch gleich ein zweites Problem gelöst: Der Qualitätsfaktor der Kurve bezieht sich auf die Zeicheneinheiten des Koordinatensystems. Hätten wir also eine Kurve mit Punkten, die nur wenige Einheiten auseinander liegen, so würde bei sehr starker Vergrößerung des Views wiederum das Problem auftauchen, dass die einzelnen Strecken, aus denen die Bezierkurve zusammengesetzt wird, sichtbar werden. Da jedoch zunächst die Transformationen durchgeführt werden und erst dann die Kurve berechnet wird, wird die Qualität am Zielkoordinatensystem (Bildschirm, Drucker usw.) ermittelt. Darauf kommt es für unser Auge an.
•
Abnehmende Varianz: Wenn Sie darauf achten, werden Sie bemerken, dass alle hier dargestellten Kurven innerhalb des Vieleckes liegen, welches durch Endund Stützpunkt aufgezogen wird. Man sagt auch: Bezierkurven schwingen 56
Kapitel 1
Graphische Ausgabe
nicht über. Das ist eine wichtige Eigenschaft, um die räumliche Ausdehnung einer Kurve abschätzen zu können: Liegt ein Punkt nicht innerhalb des Viereckes, so kann er auch nicht auf der Kurve liegen. Wird also etwa nach einem Mausklick des Benutzers danach gesucht, ob der Benutzer auf eine Kurve geklickt hat, so können in einer komplizierten Zeichnung sehr schnell zahlreiche Objekte ausgeschieden werden, weil der Mausklick nicht einmal innerhalb des Viereckes liegt. Das sorgt für ein sehr gutes Laufzeitverhalten.
Unmittelbare Ausgabe von Standardelementen Wir sollten jedoch mal Schluss mit der Theorie machen und anfangen, Sourcecode zu schreiben. Als Erstes legen Sie bitte eine neue Klasse CardStackView an, wobei Sie als Vorlage wieder Objective-C class | NSView wählen. Um auch das Ergebnis zu sehen, wechseln Sie bitte in den Interface Builder und ziehen einen Customview als Subview in unser TableBaizeView. Im Identity-Pane des Inspectors geben Sie als Klasse CardStackView an. Ziehen Sie die Karte zunächst in ein Seitenverhältnis, das in etwa dem einer Spielkarte entspricht. Schließen Sie den Nib wieder und öffnen Sie CardStackView.m. Mehr oder minder zufällig befinden sich bei NSBezierPath verschiedene Klassenmethoden, mit denen Sie unmittelbar zeichnen können. Es handelt sich gleichermaßen um Abkürzungen für die Erzeugung einer Instanz von NSBezierPath mit anschließendem Befüllen und dann der Ausgabe. Allerdings ist der Satz an Formen eher eingeschränkt: Mit +fillRect: und +strokeRect: können Sie ein Rechteck in der aktuellen Linienbzw. Füllfarbe zeichnen lassen. +strokeLineFromPoint:toPoint: bringt eine Linie in den View. Schließlich erlaubt +drawPackedGlyphs:atPoint: die Ausgabe von Zeichen. Diese Methode sollte allerdings nicht verwendet werden. (Und um die Zeichenausgabe kümmern wir uns ohnehin noch gesondert.) Damit Sie die Anwendung verstehen, machen wir ein kleines Beispiel: - (void)drawRect:(NSRect)rect { // Umrandung zeichnen NSRect cardRect = NSInsetRect( [self bounds], 0.5, 0.5 ); [NSBezierPath strokeRect:cardRect]; }
57
SmartBooks
Objective-C und Cocoa – Band 2
AUFGEPASST Da das Rechteck standardmäßig mit einer Linienbreite von 1 Einheit gezeichnet wird, müssen wir das Rechteck zunächst um 0,5 Einheiten verkleinern, damit die Linie in unserem View liegt.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 9« von der Webseite herunterladen.
Vorgefertigte Pfade Neben der unmittelbaren Ausgabe erlaubt es NSBezierPath auch, dass Instanzen erstellt werden können, die bereits Standardformen enthalten. Hier ist die Auswahl dann etwas größer. In dem -drawRect: des oben angelegten Views zeichnen wir einfach ein Rechteck mit abgerundeten Ecken. Das sieht dann in etwa so wie eine Spielkarte aus. Hierbei nutzen wir die Möglichkeit von NSBezierPath aus, uns gleich einfache Formen fertig hinzustellen: In CardStackView.m definieren wir uns zunächst eine modullokale Konstante:
GRUNDLAGEN Das static führt dazu, dass unsere Konstante sich auf unsere Datei CardStackView.m bezieht und sich unabhängig von gleichnamigen Variablen in anderen m-Dateien verhält. Ansonsten gäbe es Probleme, wenn in einer anderen Datei erneut eine solche Konstante definiert würde. Die Einzelheiten des sogenannten static-Qualifizierers haben wir n die Referenz gepackt. static const float edgeRadius = 0.1; @implementation CardStackView …
Diese benutzen wir dann in -drawRect: - (void)drawRect:(NSRect)rect { NSRect cardRect = NSInsetRect( [self bounds], 0.5, 0.5 ); CGFloat radius = NSWidth( cardRect ) * edgeRadius;
58
Kapitel 1
Graphische Ausgabe
NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:cardRect xRadius:radius yRadius:radius]; [[NSColor whiteColor] setFill]; [path fill]; [[NSColor blackColor] setStroke]; [path stroke]; }
Sie sehen hier das übliche Vorgehen: Es wird eine Instanz erzeugt, die dann ausgegeben wird. Da NSBezierPath nur die Form festlegt, können wir den Pfad einmal mit -fill ausfüllen lassen und einmal mit - stroke die Umrandung zeichnen lassen. Auch hier gilt wieder, dass für die Umrandung das Rechteck um 0,5 nach innen gesetzt werden muss, damit die Linie komplett erscheint. Das Ausfüllen würde in der Tat bis genau an die Kante gehen. Hier wäre also ein Inset nicht notwendig. Jetzt gehen wir aber so langsam mal davon aus, dass Sie die Konsequenzen der FloatKoordinaten verstanden haben. bounds - 0.5 p
bounds
Linie (1 p)
Fläche
Auf Kante genäht: Auch bei der Kombination von Stroke und Fill muss man einen Moment über die Liniendicke nachdenken.
Die Radius-Parameter geben übrigens in Einheiten die Größe der Abrundung an. Radien Radien
Rect
Rounded-Rect
Oval-In-Rect
Drei Standardformen bietet uns NSBezierPath bereits per Convenience-Allocator an.
59
SmartBooks
Objective-C und Cocoa – Band 2
Die Möglichkeiten sind auch hier nicht gerade als unendlich zu bezeichnen: Neben dem oben benutzten +bezierPathWithRoundedRect:xRadius:yRadius: existieren noch +bezierPathWithRect: und +bezierPathWithOvalInRect:, welches eine Ellipse in einem Rechteck zeichnet.
TIPP Wer bestimmte Formen häufiger benötigt, kann diese natürlich in einem selbst geschriebenen Convenience-Allocator erzeugen lassen und diesen per Kategorie der Klasse NSBezierPath hinzufügen.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 10« herunterladen.
Pfade mit mehreren Subpfaden Etwas anspruchsvoller wird es schon, wenn Sie einen Bezier-Pfad mit mehreren vorgefertigten Subpfaden haben wollen. Das Vorgehen: Zunächst erzeugt man eine Instanz von NSBezierPath, sei es mittels des Convenience-Allocators mit +bezierPath, sei es wie im vorstehenden Abschnitt mit einer Grundform. Dieser Instanz fügen wir jetzt die Standardformen hinzu. Schließlich wird er ausgegeben: - (void)drawRect:(NSRect)rect { … [path stroke]; // Leere Instanz erzeugen NSRect subrect; path = [NSBezierPath bezierPath]; // Eine Ellipse als Subpfad hinzufuegen subrect = NSMakeRect( 10.0, 10.0, 20.0, 50.0 ); [path appendBezierPathWithOvalInRect:subrect]; // Ein Rechteck als Subpfad hinzufuegen subrect = NSMakeRect( 40.0, 10.0, 20.0, 50.0 ); [path appendBezierPathWithRect:subrect]; [path stroke]; }
60
Kapitel 1
Graphische Ausgabe
HILFE Sie können das Projekt in diesem Zustand als »Card Game 11« von der Webseite herunterladen.
Freie Subpfade Häufig wird man aber Pfade aus Linien, Kreissegmenten und Bezierkurven selbst zusammensetzen wollen. Wir setzen das jetzt exemplarisch um: Nachdem wir wiederum eine leere Instanz hinzugefügt haben, setzen wir einen Startpunkt und fügen dann die einzelnen Segmente hinzu. Ist eines gemalt, so haben wir ja gleichzeitig den Startpunkt für das nächste Segment. Malen wir mal etwas in die Karte hinein. Um das Ganze etwas übersichtlicher zu gestalten, bauen wir uns eine neue Methode, die eine einzelne Karte zeichnet und die in -drawRect: benutzt wird: - (void)drawCardFaceWithSuit:(NSUInteger)suit value:(NSUInteger)value inRect:(NSRect)rect { // Mittelpunkt des Kartenrechteckes NSPoint center; center = NSMakePoint( NSMidX( rect ), NSMidY( rect )); // Vier Punkte NSPoint left = NSMakePoint( NSPoint top = NSMakePoint( NSPoint right = NSMakePoint( NSPoint bottom = NSMakePoint(
darum, an den Spitzen eines Karos center.x - NSWidth( rect ) * 0.2, center.y ); center.x, center.y + NSWidth( rect ) * 0.2 ); center.x + NSWidth( rect ) * 0.2, center.y ); center.x, center.y - NSWidth( rect ) * 0.2 );
// Leere Instanz erzeugen NSBezierPath* path = [NSBezierPath bezierPath]; // Startpunkt festlegen [path moveToPoint:left]; // Eine gerade Linie zum oberen Punkt [path lineToPoint:top];
61
SmartBooks
Objective-C und Cocoa – Band 2
// Ein Viertelkreis zum rechten Punkt [path appendBezierPathWithArcFromPoint:center toPoint:right radius:NSWidth( rect ) * 0.2]; // Eine Bezierkurve zum unteren Punkt [path curveToPoint:bottom controlPoint1:NSMakePoint( right.x - 12, right.y ) controlPoint2:NSMakePoint( bottom.x, bottom.y + 12 )]; // schliessen [path closePath]; [[NSColor redColor] setFill]; [path fill]; } - (void)drawRect:(NSRect)rect { … [path stroke]; [self drawCardFaceWithSuit:0 value:1 inRect:cardRect]; }
Die neue Methode -drawCardFaceWithSuit:value:inRect: erhält als Parameter das Rechteck der zu zeichnenden Karte. Beachten Sie bitte, dass das eine Änderung zu -drawRect: ist: Dort wurde als Parameter die neu zu zeichnende Fläche übergeben. Die Zeichenfläche (des Views) ergab sich aus -bounds (NSView). Hier übergeben wir gleich die Zeichenfläche. Den Anfang der Methode müssten Sie eigentlich schon gut verstehen: Es wird mit ein paar Makros zunächst der Mittelpunkt des Kartenrechteckes ermittelt und von dort aus werden vier Punkte erzeugt, die die Ecke eines Karos um den Mittelpunkt bilden. Als »Radius« werden dabei 20 Prozent der Kartenweite verwendet. Instanz erzeugen und starten Jetzt geht es aber in die neuen Sachen: Zunächst wird eine Instanz erzeugt, die leer ist. Da sie leer ist, befindet sich in ihr auch noch nicht unsere Schildkröte. Diese müssen wir zunächst auf einen Startpunkt setzen, was wir mit -moveToPoint: machen. Das ist jetzt also unsere aktuelle Zeichenposition, die man mit -currentPoint abfragen kann.
62
Kapitel 1
Graphische Ausgabe
Linien Als Nächstes wird dann eine Linie zum oberen Punkt (top) gezeichnet, wozu wir -lineToPoint: verwenden. Für beide Methoden existieren übrigens Verwandte, die die relative Bestimmung des Zielpunktes erlauben: -relativeMoveToPoint: und relativeLineToPoint:. Man könnte also auch die Linie wie folgt ziehen: [path relativeLineToPoint:NSMakePoint( NSWidth( rect ) * 0.2, NSWidth( rect ) * 0.2 )];
Die relativen Angaben beziehen sich dann eben auf die aktuelle Position (Current-Point). Wie auch immer ziehen wir die Linie, schicken also unsere Schildkröte über die Leinwand. Der Zielpunkt der Linie, also die obere Spitze unseres Karos, ist danach die neue aktuelle Position. Sie können auch einen ganzen Linienzug mit der Methode -appendBezierPath WithPoints:count: anschließen. Sollte es noch keine aktuelle Zeichenposition geben, weil Sie weder in den Pfad gezeichnet haben, noch mit ein -moveToPoint: starteten, wird ab dem ersten Punkt gezeichnet. Andernfalls wird zunächst eine Linie von der aktuellen Zeichenposition zum ersten Punkt gezogen. Nach der Methode liegt die aktuelle Position also erneut auf dem letzten Punkt.
GRUNDLAGEN Als Parameter erhält die Methode eine Variable vom Typen NSPointerArray. Hierbei handelt es sich um ein C-Array von NSPoint. Schlagen Sie dazu bitte in der Objective-C-Referenz nach, wenn Sie diese Methode einmal anwenden wollen. Kreissegmente Im nächsten Schritt zeichnen wir ein Kreissegment (Arc). Es soll ein Viertelkreis zum rechten Punkt sein, der konkav (nach innen gewölbt) in unserer Figur liegt. Leider ist das Zeichnen von Kreissegmenten alles andere als einfach: Sie müssen dabei bedenken, dass ein Kreissegment drei »Freiheitsgrade« hat: Die Lage (etwa durch den Mittelpunkt bestimmt), den Radius und den Anfangs- und Endwinkel. Wenn man wirklich alle drei Freiheitsgrade zulässt, dann benötigt man erstaunlich viel Information, um das Kreissegment festzulegen. In unserem Beispiel etwa lässt sich nur ein Viertelkreis vom oberen zum unteren Punkt zeichnen, der konkav in der Figur liegt. Aber eben Viertelkreis: Wenn man auch andere Ausschnitte zulässt, dann ergeben sich mehrere Möglichkeiten für das Kreissegment (genau genommen unendlich viele). 63
SmartBooks
Objective-C und Cocoa – Band 2
Viele Umwege führen nach Rom: Ein Kreissegment von einem Punkt zum anderen zu zeichnen, verlangt mehr als die Angabe der Punkte.
Die zweite wichtige Eigenschaft ist, dass der Subpfad durchgezogen bleiben soll. Lassen sich also etwa aufgrund eines zu kleinen Radius die Punkte nicht verbinden, so muss vom Startpunkt eine Hilfslinie gezeichnet werden, damit der Pfad geschlossen bleibt. Der Zielpunkt ist dann nicht der neue aktuelle Punkt, sondern lediglich ein Hilfspunkt. Deutlicher wird das bei der von uns verwendeten Methode -appendBezierPath WithArcFromPoint:toPoint:radius:. Sie zeichnet ein Kreissegment, das sich tangential zu den Schenkeln eines Dreieckes verhält, welches sich durch Aufspannen jeweils von From-Point zum Current-Point bzw. To-Point ergibt unter Berücksichtigung des angegebenen Radius. Alles klar, nicht wahr? Die Apple-Dokumentation erklärt das ziemlich genau so, und kein Mensch versteht das. Daher machen wir das mal mit einer Graphik, die vieles erläutern dürfte: from
to current
Cocoa zieht zuweilen schwer verständliche Kreise.
Unmittelbar aus dem Radius ergibt sich natürlich ein Kreis ohne besondere Lage. Stellen Sie sich das als Aufsicht auf einen Ball vor. Dieser Ball wird in die Ecke um den From-Point geschoben, bis er reinpasst. Würden wir also den Radius verkleinern, so wandert der Ball näher an den From-Point, vergrößern wir ihn, so marschiert er wieder heraus. Da nur »zufällig« der Berührungspunkt des Kreises mit dem Schenkel an der Strecke Current-From genau im Current-Point liegt, wird 64
Kapitel 1
Graphische Ausgabe
zudem zunächst eine Linie vom Current-Point bis zum Berührungspunkt gezogen. Das sorgt dafür, dass der Pfad geschlossen bleibt. Zum To-Point geschieht dies nicht, weshalb wir gegebenenfalls noch einmal ein -lineToPoint: hinterherschicken müssten, damit dies wirklich unser neuer Current-Point wird. In unserem Beispiel oben setzen wir allerdings den From-Point genau in die Ecke eines rechtwinkligen Dreiecks und malen den Kreis mit dem richtigen Radius, so dass tatsächlich nur ein Viertelkreisbogen vom Current-Point zum To-Point gezeichnet wird.
current = top
from = center
to = right
Mit etwas Hirnschmalz bekommt man passgenaue Kreisbögen hin.
Weitere Methoden sind -appendBezierPathWithArcWithCenter: radius: start Angle:endAngle: und -appendBezierPathWithArcWithCenter:radius:startAngle: endAngle:clockwise:, mit denen um den angegebenen Mittelpunkt herum ein Kreisbogen gezeichnet wird, der bei den übergebenen Winkeln beginnt und endet. Der Winkel bemisst sich dabei ab der X-Achse gegen den Uhrzeigersinn. Wenn bereits eine aktuelle Position existiert, wird von dieser eine Linie zum Startpunkt gezogen. Nach der Methode befindet sich die aktuelle Position am Endpunkt. Man könnte obigen Code also auch abändern: NSPoint topRight = NSMakePoint( right.x, top.y ); [path appendBezierPathWithArcWithCenter:topRight radius:NSWidth( rect ) * 0.2 startAngle:180.0 endAngle:270.0];
Bezier-Kurven Schließlich findet sich im Code auch ein Beispiel für die letzte Segmentart, die eigentliche Bezier-Kurve. Besprochen haben wir das zur Genüge, so dass Sie sich bitte lediglich den Aufruf merken.
65
SmartBooks
Objective-C und Cocoa – Band 2
HILFE Sie können das Projekt in diesem Zustand als »Card Game 12« von der Webseite herunterladen. Pfad abschließen Am Ende wird der Pfad noch mit -closePath geschlossen. Hierbei wird eine Linie vom Current-Point, also der aktuellen Zeichenposition zum Startpunkt des Pfades gezogen. Wie bereits erwähnt ist das nur für Fülloperationen notwendig, wobei dann NSBezierPath automatisch den Subpfad schließt. Kommentieren Sie bitte die Zeile mit dem closePath aus und starten Sie das Programm. Es hat sich nichts geändert. // schliessen // [path closePath]; …
Lassen wir nur den Linienzug zeichnen, so wird der Pfad nicht automatisch geschlossen: … [[NSColor redColor] setStroke]; [path stroke]; }
Glyphen Glyphen sind die graphische Darstellung von Schriftzeichen. So existieren etwa für den Buchstaben Z die Glyphen Z Z Z Z. (Der Programmierer würde sagen, dass die Schriftzeichen das Model sind, die Glyphen die Views.)
GRUNDLAGEN Es kommt sogar noch schlimmer, wie Sie im Kapitel über das Textsystem näher lernen werden: Es gibt Schriftzeichen, die aus verschiedenen Glyphen zusammengesetzt sein können, und es gibt Glyphen, die mehrere Schriftzeichen auf einmal darstellen. Daher ist der gleich verwendete Glyphen-Index nicht deckungsgleich mit dem Unicode des Zeichens, sondern so etwas wie die Nummer des Glyphen im verwendeten Font. Man kann das freilich vom Textsystem ermitteln lassen. Hier wollen wir uns jedoch auf die Graphik der Glyphen konzentrieren.
66
Kapitel 1
Graphische Ausgabe
Glyphen sind ein Zwischending zwischen Subpfaden und Elementen. Sie benötigen bereits einen aktuellen Punkt, also einen geöffneten Subpfad. Sie führen auch zu einem neuen aktuellen Punkt. Allerdings sind sie in sich abgeschlossene Pfade, so dass sie den Subpfad beenden und eine neue aktuelle Position eröffnen. Sie werden das auch gleich sehen. Da wir für unser Kartenspiel ein paar Glyphen benötigen, ist es eine praktische Angelegenheit, eine Kategorie BezierPathCardsAddition hinzuzufügen, die die Symbole für die Kartenfarbe (Karo, Herz, Pik, Kreuz) bzw. die Kartenwerte (As, 2 bis 10, Bube, Dame, König) einem Pfad hinzufügt. Machen wir uns dran: Erstellen Sie eine neue Datei für die Kategorie. Da es hierfür keine Vorlage gibt, nehmen wir einfach als Vorlage Objective-C class | NSObject. Die Klassendeklaration im Header müssen wir freilich zur Kategorie machen und dann unsere beiden Methoden einbauen: @interface NSBezierPath( BezierPathCardsAddition ) - (void)appendBezierPathForSuit:(NSUInteger)suit; - (void)appendBezierPathForValue:(NSUInteger)index; @end
Spannender für die hiesige Thematik ist dann freilich die Implementierung: @implementation NSBezierPath( BezierPathCardsAddition ) - (void)appendBezierPathForSuit:(NSUInteger)suit { NSFont* font; font = [NSFont fontWithName:@"Apple Symbols" size:40]; switch( suit ) { case 0: [self appendBezierPathWithGlyph:389 break; case 1: [self appendBezierPathWithGlyph:388 break; case 2: [self appendBezierPathWithGlyph:383 break; case 3: [self appendBezierPathWithGlyph:386 break; }
inFont:font];
inFont:font];
inFont:font];
inFont:font];
67
SmartBooks
Objective-C und Cocoa – Band 2
} - (void)appendBezierPathForValue:(NSUInteger)index { NSFont* font; font = [NSFont fontWithName:@"Helvetica" size:40]; // Reihenfolge ist: 0 A 2 3 4 5 6 7 8 9 1 B D K switch( index ) { case 1: // A [self appendBezierPathWithGlyph:36 inFont:font]; break; case 10: // 1 [self appendBezierPathWithGlyph:20 inFont:font]; break; case 11: // B [self appendBezierPathWithGlyph:37 inFont:font]; break; case 12: // D [self appendBezierPathWithGlyph:39 inFont:font]; break; case 13: // K [self appendBezierPathWithGlyph:46 inFont:font]; break; default: // 0 2 3 4 5 6 7 8 9 [self appendBezierPathWithGlyph:19+index inFont:font]; break; } } @end
Sie sehen also, dass wir für gewisse Indexe bestimmte Glyphen verwenden. Die Glyphencodes kann man – wenn man nicht das Textsystem bemüht – aus der OSX-Zeichenpalette heraussuchen: Sie können dort ganz oben auf Glyphendarstellung schalten und dann eine Schrift auswählen. (Die Zeichenpalette können Sie sich über die Systemeinstellungen | Landeseinstellungen | Tastaturmenü in der Menüzeile anzeigen lassen.) Der Glyph ist wie gesagt der Index in einem bestimmten Font. Er ist als eigener Datentyp NSGlyph codiert, wobei es sich allerdings um einen simplen UnsignedInteger handelt, so dass man diesen als Konstante angeben kann. Da sich der Glyph auf einen bestimmten Font bezieht, muss eine Font-Instanz mitgeliefert werden, 68
Kapitel 1
Graphische Ausgabe
damit der Pfad von -appendBezierPathWithGlyph:inFont: ermittelt werden kann. Font ist hier im Sinne von Computer-Zeichensatz zu verstehen, also die kodierte graphische Darstellung von Zeichen. Die Font-Intanz erhalten wir wieder eingangs der Methode mit +fontWithName:size: (NSFont), indem wir den Namen des Fonts (Helvetica bzw. Apple Symbols) und dazu die Größe angeben. So wäre der kursive Font mit Helvetica Oblique zu erreichen. Die so geschriebene Kategorie verwenden wir gleich in CardStackView.m zur Ausgabe: #import "CardStackView.h" #import "BezierPathCardsAddition.h" … - (void)drawCardFaceWithSuit:(NSUInteger)suit value:(NSUInteger)value inRect:(NSRect)rect { // Mittelpunkt des Kartenrechteckes NSPoint center; center = NSMakePoint( NSMidX( rect ), NSMidY( rect )); NSBezierPath* path = [NSBezierPath bezierPath]; [path moveToPoint:center]; [path appendBezierPathForSuit:0]; NSPoint suitRect = [path currentPoint]; [path appendBezierPathForValue:1]; NSPoint valueRect = [path currentPoint]; if( suit < 2 ) { [[NSColor redColor] set]; } else { [[NSColor blackColor] set]; } [path fill]; // Punkte anzeigen [[NSColor blackColor] set]; NSRect current; current.size = NSMakeSize( 8.0, 8.0 ); current.origin = NSMakePoint( suitRect.x-5.0, suitRect.y-5.0 ); path = [NSBezierPath bezierPathWithOvalInRect:current]; [path stroke];
69
SmartBooks
Objective-C und Cocoa – Band 2
current.origin = NSMakePoint( valueRect.x-5.0, valueRect.y-5.0 ); path = [NSBezierPath bezierPathWithOvalInRect:current]; [path stroke]; }
Wenn Sie jetzt das Programm laufen lassen, sehen Sie die entsprechenden Zeichen und mit den Kreisen markiert die neuen aktuellen Positionen nach Anhängen des Glyphen. Beim Karo ist es augenfällig: Es wurde am Ende ein -moveToPoint: vorgenommen, so dass der Subpfad geschlossen wurde. Dies ist außerordentlich praktisch, da so das nächste Zeichen richtig gesetzt ist. Löschen Sie wieder den letzten Teil einschließlich der Zeilen mit suitRect und valueRect und lassen Sie sich die Glyphen als Umrandung ausgeben: - (void)drawCardFaceWithSuit:(NSUInteger)suit value:(NSUInteger)value inRect:(NSRect)rect { // Mittelpunkt des Kartenrechteckes NSPoint center; center = NSMakePoint( NSMidX( rect ), NSMidY( rect )); NSBezierPath* path = [NSBezierPath bezierPath]; [path moveToPoint:center]; [path appendBezierPathForSuit:0]; [path appendBezierPathForValue:1]; if( suit < 2 ) { [[NSColor redColor] set]; } else { [[NSColor blackColor] set]; } [path stroke]; }
Sie sehen dann, dass auch beim A die Glyphen nicht als Linien, sondern als Flächen definiert sind. Ändern Sie die letzte Zeile das »stroke« wieder in »fill«.
70
Kapitel 1
Graphische Ausgabe
AUFGEPASST Man kann die einzelnen Segmente auch wieder abfragen. Dabei kommen jedoch leider die Untiefen von C zum Vorschein. Leider und Gott sei’s gedankt: Wir verwenden dies als Beispiel in der Referenz. Legen Sie also bitte schon einmal das aktuelle Projekt zur Seite. Sie benötigen es wieder.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 12a« von der Webseite herunterladen.
Globale Eigenschaften Wie bereits ausgeführt, wird die komplette durch die Pfade gebildete Figur als eine Instanz von NSBezierPath gespeichert. Dies hat den Vorteil, dass eine solche Zeichnung komplett als Eigenschaft gespeichert werden kann. So ist es etwa in unseren Beispielen so, dass bei der Zeichnerei jedes Mal die Pfade neu erstellt werden. Das ist natürlich eigentlich nicht erforderlich: Man könnte die verschiedenen Pfade vorab im -init erzeugen und als Eigenschaften des Views ablegen. Beispielsweise: @interface CardStackView : NSView { NSBezierPath* karoPfad; } @property( copy ) NSBezierPath karoPfad; @end @implementation CardStackView - (id)initWithFrame:… self = [super init]; if( self ) { self.karoPfad = … } … }
Eine Vielzahl von Pfaden lässt sich in Collections (NSArray etwa) stecken usw. Man hat halt alle Vorteile einer gekapselten Instanz. Da Bezier-Pfade das CodingProtokoll beherrschen, kann man sie sogar abspeichern. Ein weiterer Vorteil der Kapselung liegt darin, dass sich der Bezier-Pfad als Ganzes bearbeiten lässt, etwa drehen, verschieben, vergrößern, verzerren usw. Hierzu verwendet man sogenannte Transformationen, die wir in einem gesonderten Abschnitt besprechen, da sie an einigen Stellen verwendet werden. 71
SmartBooks
Objective-C und Cocoa – Band 2
Graphische Attribute Schließlich wird ein Satz von Attributen wie Linienstärke zu jeder Instanz von NSBezierPath gespeichert. Dies ist einerseits vorteilhaft, weil man diese Attribute schnell für die komplette Form ändern kann. Andererseits führt dies zu der häufigen Nachfrage, ob man nicht auch Linien unterschiedlicher Stärke in einem BezierPfad unterbringen kann. Und die Antwort lautet »Nein!«. Wenn Sie also eine Form haben, die unterschiedliche Attribute benötigt, so müssen Sie den Pfad in mehrere Bezier-Pfade aufspalten. Da dabei geschlossene Pfade »zerrissen« werden können, muss für ein etwaiges Füllen noch ein weiterer Pfad her.
=
+
+
Füllpfad
Umriss 1
Umriss 2
Aus 3 mach 1: Wechseln die Attribute innerhalb eines Pfades, so sind mehrere Pfade zu erzeugen.
Meist kennen Sie die einzelnen Punkte in den Pfaden, so dass Ihnen das einzelne Zusammensetzen keine Probleme bereitet. Gerade aber bei Glyphen und Kreisbögen kann es sein, dass Ihnen das nicht bekannt ist. In diesem Falle können Sie aber beim Zeichnen des Pfades mit -currentPoint den aktuellen Punkt erfragen. Stellen Sie sich etwa als Beispiel vor, der Symbolglyph soll mit einer dicken Linie gezeichnet werden, dann soll mit einer dünnen Linie der Wert folgen, schließlich wieder mit einer dicken Linie (also im ersten Pfad) eine einfache Linie: // Zwei Bezier-Pfade erzeugen und Attribute setzen NSBezierPath* path1 = [NSBezierPath bezierPath]; [path1 setLineWidth:1.0]; NSBezierPath* path2 = [NSBezierPath bezierPath]; [path1 setLineWidth:5.0]; // Ersten Pfad anfangen [path1 moveToPoint:center]; [path1 appendBezierPathForSuit:0]; // Zum anderen Pfad huepfen: [path2 moveToPoint:[path1 currentPoint]]; [path2 appendBezierPathForValue:1];
72
Kapitel 1
Graphische Ausgabe
// Zurueck zum ersten [path1 moveToPoint:[path2 currentPoint]]; [path1 relativeLineToPoint:NSMakePoint( 0.0, 10.0 )]; if( suit < 2 ) { [[NSColor redColor] set]; } else { [[NSColor blackColor] set]; } [path1 stroke]; [path2 stroke];
Defaults Die Attribute eines Pfades können entweder mittels einer Instanzmethode wie -setProperty: für den konkreten Pfad gesetzt werden oder als Default mittels einer Klassenmethode +setDefaultProperty. Werden sie als Default gesetzt, so hat dies zwei Auswirkungen:
• •
Neue Instanzen erhalten diese Attribute. Bei der direkten Ausgabe von Standardelementen (siehe oben) werden ebenfalls diese Attribute verwendet.
Linienstärke und Formen Das wohl prominenteste Attribut ist die Linienstärke. Die entsprechende Eigenschaft heißt »Line-Width«. Die Linienstärke wird in Einheiten des Koordinatensystems gemessen. Bereits am Anfang des Kapitels hatten wir Sie abstrakt darauf hingewiesen, dass Linienstärken zu der Notwendigkeit von Linienendformen (Line-Cap-Style) und Linienübergängen (Line-Join-Style) führen müssen.
Square
Butt
Round
Cocoa bietet drei Linienendstile.
73
SmartBooks
Objective-C und Cocoa – Band 2
Bei Linienendstilen ist das sofort einsichtig. Auch die zur Verfügung gestellten Linienübergänge sind einsichtig: Bei Miter (spitz) werden die äußeren Kanten der Linien verlängert, so dass sie sich treffen. Dies bildet dann den Übergang. Bei Bevel (abgeflacht) werden die Endpunkte der äußeren Kanten unmittelbar mit einer Linie verbunden. Schließlich existiert Round (abgerundet), welches die weicheste Form darstellt.
Bevel
Miter
Rounded
Auch die Linienübergänge kennen verschiedene Formen.
Beim spitzen Übergang existiert jedoch noch das Problem, dass kleine Winkel zu absurden Spitzen führen können. Aus diesem Grunde existiert die Eigenschaft Miter-Limit, die das begrenzt. Sie gibt das erlaubte Verhältnis zwischen Spitzenlänge und Strichstärke an. it
Lim
Cocoa setzt den Spitzen Grenzen.
Zur Anwendung sei noch darauf hingewiesen, dass es hier vorgefertigte Konstanten gibt, die aus einem sogenannten Enum stammen. Die genaue Arbeitsweise entnehmen Sie bitte wieder der Referenz. Lassen Sie sich aber nicht verwirren: Hier ist lediglich wichtig, dass Sie die in der Dokumentation angegebenen Stile wie ganz normale Konstanten verwenden können. Das Autovervollständigen von Xcode leistet hier gute Hilfe: [path setLineCapStyle:NSButtLineCapStyle];
74
Kapitel 1
Graphische Ausgabe
Linienmuster Eine weitere Einstellmöglichkeit ist das Linienmuster. Hierbei werden ganz besonders eklige C-Arrays verwendet, richtiges Teufelszeug. Vereinfacht gesagt handelt es sich dabei um die Vervielfältigung einer Variablen. Auch hierzu finden Sie Ausführungen im Referenzteil. Für die Anwendung benötigen Sie allerdings auch hier keine tiefer gehenden Kenntnisse. Sie speichern einfach eine Folge von Längen für das Muster. Machen wir ein Beispiel anhand einer waagerechten Linie. Fügen Sie folgenden Code ein: - (void)drawCardFaceWithSuit:(NSUInteger)suit value:(NSUInteger)value inRect:(NSRect)rect { … CGFloat linePattern[] = { 15.0, 7.0, 10.0, 7.0 }; path = [NSBezierPath bezierPath]; [path moveToPoint:NSMakePoint( 10.5, 10.5 )]; [path lineToPoint:NSMakePoint( 120.5, 10.5 )]; [path setLineDash:linePattern count:4 phase:0.0]; [path setLineWidth:5.0]; [path stroke]; }
Hier wird also eine Linie als Bezier-Pfad erzeugt. Als Muster wird angeordnet, dass zunächst eine Strecke von 15 Einheiten gezeichnet werden soll, dann eine Strecke von 7 Einheiten ungezeichnet bleibt, es folgt dann erneut eine Strecke von 10 Einheiten. Abgeschlossen wird das Muster mit einer Lücke von 7 Einheiten. Ist die Linie länger als das Muster, so wird es von vorne wiederholt. Bitte schauen Sie sich das Ergebnis auch mit Pixie an. Die gezeichneten Teillinien sind übrigens »ganz normale« Linien mit Endstilen. Probieren Sie es mal aus: [path setLineWidth:5.0]; [path setLineCapStyle:NSRoundLineCapStyle]; [path stroke];
Sie sehen jetzt entsprechende Rundungen. Der Phase-Parameter dient dazu, die Linie zum Muster zu verschieben. Damit kann man etwa mehrfarbige Linien zeichnen, wenn man das Muster und die Phase geschickt anordnet: CGFloat linePattern[] = { 7.0, 7.0 }; path = [NSBezierPath bezierPath]; [path moveToPoint:NSMakePoint( 10.5, 10.5 )];
75
SmartBooks
[path [path [path [path [path
Objective-C und Cocoa – Band 2
lineToPoint:NSMakePoint( 120.5, 10.5 )]; setLineDash:linePattern count:2 phase:0.0]; setLineWidth:5.0]; setLineCapStyle:NSButtLineCapStyle]; stroke];
[[NSColor greenColor] set]; [path setLineDash:linePattern count:2 phase:7.0]; [path stroke];
Löschen Sie den neuen Code wieder. Füllregel Sie haben ja schon Pfade ausgefüllt. Es bleibt dabei jedoch das Problem, dass Pfade in Pfaden liegen können. Dann ist es nicht mehr eindeutig, welche Teile ausgefüllt werden sollen: Stellt der innere Pfad ein Loch da? Um das Verhalten zu beeinflussen, existiert die Eigenschaft Winding-Rule. Diese ist als Enum mit den Konstanten NSNonZeroWindingRule und NSEvenOddWindingRule definiert. Der Gedanke ist, dass sich für jeden Punkt bestimmen lässt, ob er sich »innerhalb« oder «außerhalb« des Pfades befindet, indem man von ihm eine Linie in die Unendliche zeichnet und die Anzahl der Kreuzungen mit der Umrandung zählt. Allerdings ist die Zählung dabei anders: 2
1
3
2
Bei der Even-Odd-Winding-Rule wird einfach gezählt.
•
Even-Odd bedeutet, dass sich jeder Punkt innerhalb einer Fläche befindet (also beim Ausfüllen gesetzt wird), für den die Anzahl der Kreuzungen mit den Umrandungen ungerade ist. 76
Kapitel 1
Graphische Ausgabe
+1
+2
+1
0
Gleichgerichtete Subpfade sind bei der Non-Zero-Winding-Rule keine Löcher, …
•
Bei Non-Zero ist das nicht so einfach: Hier werden nicht einfach die Kreuzungen gezählt, sondern auch berücksichtigt, ob wir es mit einer Links-Kreuzung oder Rechts-Kreuzung zu tun haben. Für die eine Richtung gibt es einen Pluspunkt, für die andere einen Minuspunkt. Beträgt die Summe 0, so befindet sich der Punkt außerhalb der Umrandung, andernfalls innerhalb. Das führt zu dem Ergebnis, dass bei ineinandergesteckten Figuren die Richtung entscheidet. +1
+2
+1
+2
… entgegengerichtete Subpfade jedoch schon.
77
SmartBooks
Objective-C und Cocoa – Band 2
Bildausgabe und NSImage Zuweilen möchte man ein fertiges Bild auf den Bildschirm bringen. Eine – und vor allem eine bequeme – Möglichkeit ist es, einen Imageview zu verwenden, der ja auch Subview eines unserer Views sein kann. Hierzu müssen wir nichts mehr erzählen. Will man allerdings selbst ein Bild zeichnen, so wird die Sache komplexer. Fügen Sie ein x-beliebiges Bild dem Projekt hinzu, indem Sie es in die Gruppe Resources der Projektleiste Groups & Files ziehen. Im Dialog sorgen Sie bitte dafür, dass in der obersten Zeile Copy items into destination group’s folder (if needed) angewählt ist und unten in der Liste Add to targets das Häkchen vor dem aktuellen Projekt gesetzt ist. So ist gewährleistet, dass das Bild später von Xcode beim Build-Prozess in das fertige Applikationsbundle kopiert wird (Band I, S. 674 ff.). Benennen Sie das Bild mit Bild, wobei das Dateisuffix (jpg, tiff usw.) beibehalten werden sollte. Wie bereits im ersten Band erwähnt, kann man dieses Bild nun vom Code aus benutzen: - (void)drawRect:(NSRect)rect { … NSImage* image = [NSImage imageNamed:@"Bild"]; NSLog( @"%@", image ); }
Im Log sehen Sie die entsprechenden Angaben zum Bild: >…
NSImage 0x181490 Name=Bild Size={72, 82} Reps=( NSBitmapImageRep 0x1827e0 Size={72, 82} ColorSpace=NS CalibratedRGBColorSpace BPS=8 BPP=24 Pixels=72x82 Alpha=NO Planar=NO Format=0
)
Sie sehen bei genauer Betrachtung, dass zum Image selbst nur zwei Werte gespeichert sind, nämlich der Name und die Größe. Danach folgt eine Aufzählung von »Reps«, wobei hier nur eine gelistet ist. Was ist das? Wichtig ist zu verstehen, dass NSImage ist nur eine Hülle für Bilder darstellt. Die tatsächliche Ablage der Daten erfolgt in einer Image-Representation (Image-Rep, Bildrepräsentierung). Man kann sich das als das Bildformat vorstellen. Dabei kann eine Instanz von NSImage mehrere Repräsentationen haben, die sich etwa in der
78
Kapitel 1
Graphische Ausgabe
Farbtiefe unterscheiden. Das System wählt dann bei der Ausgabe die passende Repräsentation für einen Ausgabevorgang. Image-Representations sind Instanzen der Klasse NSImageRep, die wiederum über einige Unterklassen verfügt. Die wichtigste Unterklasse ist die hier vorgefundene NSBitmapImageRep, welche eine Rastergraphik darstellt, also die Anordnung von Pixeln in einem Gittersystem. Aber Cocoa unterstützt etwa auch Image-Representations für PDF-Dokumente oder Encapsulated-PostScript (EPS). Klasse
Unterstützte ex- Beschreibung terne Formate
NSBitmapImageRep JPEG, TIFF, GIF, PNG …
Rastergraphik mit Farbwerten für Pixel
NSCIImageRep
Core-Image
Core Image-Bild, welches wiederum verschiedene Formate unterstützt.
NSPDFImageRep
PDF
Ein PDF als Bild
NSEPSImageRep
PostScript
Eine PostScript-Datei als Bild
NSPICTImageRep
PICT
Darstellung des OS-9-Formates PICT.
NSCustomImageRep Beliebig
Ein eigenes Format. Das Zeichnen des Bildes wird von einem Delegate übernommen.
NSCachedImageRep Intern
Ein vorgezeichnetes Bild, interne Verwendung
Welche Bildformate unterstützt werden, liefert die Klassenmethode +imageTypes (NSImage). Hierbei sind auch diejenigen Typen aufgelistet, die zwar nicht unmittelbar unterstützt werden, für die aber im System ein Importfilter registriert wurde. Die eigentlichen Dateiformate erhält man mit +imageUnfilteredTypes (NSImage). Die abgekürzten Werte für die Bitmap-Image-Representation haben eine einfache Bedeutung:
•
BPS (Bits per Sample, Farbtiefe) gibt an, wie viele Bits pro Farbkanal verwendet werden. Für eine RGB-Farbinformation werden etwa bei einem Wert von 8 insgesamt 24 Bits zur Kodierung verwendet. Dies hat übrigens keinerlei Bedeutung für die Frage, wie viele Bits ein Pixel belegt. Denn es ist nicht gesagt, wie die einzelnen Farbkanäle im Speicher angeordnet sind.
•
BPP (Bits per Pixel, Speichergröße) liefert diese Information. Sie ist hier entsprechend der drei Farbkanäle mit jeweils 8 Bit 24 Bit. Wie bereits gesagt, ist dies aber nicht zwingend. So könnten etwa die 3 Farbkanäle des einfachen Zu79
SmartBooks
Objective-C und Cocoa – Band 2
griffes halber in insgesamt 32 Bit untergebracht sein, wobei 8 Bit unbenutzt bleiben.
•
Alpha gibt an, ob das Bildformat Transparenz kennt. Ich habe im Beispiel ein JPG verwendet, welches dies nicht unterstützt.
•
Planar gibt an, ob die einzelnen Farbkanäle in einem Speicherbereich liegen oder in verschiedenen »Auszügen« in getrennten Speicherbereichen.
•
Format enthält Informationen dazu, wie der Alphakanal kodiert ist.
Konzentrieren wir uns aber kapitelgemäß auf die Ausgabe solcher Bilder: - (void)drawRect:(NSRect)rect { … NSImage* image = [NSImage imageNamed:@"Bild"]; [image drawInRect:NSMakeRect(10.0, 10.0, 100.0, 100.0 ) fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0]; }
Die beiden ersten Parameter geben das Zielrechteck (im View) bzw. das Quellrechteck (im Image) an. Wird für das Quellrechteck ein leeres Rechteck wie hier übergeben, so wird automatisch das gesamte Image gezeichnet. In jedem Falle erfolgt eine automatische Skalierung, wobei das Seitenverhältnis (Aspect-Ratio) nicht erhalten bleibt. Soll dies der Fall sein, so sind die Rechtecke entsprechend zu berechnen. Mit dem operation-Parameter wird angegeben, wie Quellbild und Zielbild (dies ist hier der Inhalt des Views vor der Zeichenoperation) zu dem resultierenden Bild verrechnet werden. Hier verwenden wir Source-Over, was bedeutet, dass überall dort, wo unser Bild (Source) nicht-transparent ist, das Bild erscheint, an transparenten Stellen jedoch der ursprüngliche Bildschirminhalt. Am besten, Sie verwenden mal ein Bild, welches transparente Stellen hat, und setzen die Koordinaten so, dass es teilweise unsere bisherige Zeichnung im View überdeckt: An den transparenten Stellen erscheint unsere bisherige Zeichnung. Die einzelnen Modi sind in der Dokumentation erläutert. Wenn Sie hiermit herumprobieren wollen, seien Sie aber schon vor einer Falle gewarnt: Sowohl betreffend des Fensters als auch betreffend der einzelnen Views nimmt Quartz einige Annahmen vor, die eine echte Transparenz ausschließen. Vor allem wird ein gemeinsamer Puffer für mehrere Views angelegt, in dem sich dann eine »Hintergrundfarbe« befindet. Sie sehen dann Schwarz anstelle von Transparenz. Um das anschaulich zu machen: Wenn Sie den kompletten Bildschirm mit einer vollständig transparenten Farbe vollzeichnen würden, würden Sie ja auch nicht erwarten, in den Monitor hineinschauen 80
Kapitel 1
Graphische Ausgabe
zu können. Man kann dieses Problem lindern, wenn man – wie später erläutert wird – Core-Animation-Layer für jedes View erzeugt, da diese dann unabhängig voneinander sind. Der Parameter fraction gibt die Deckkraft des Bildes an. Wenn Sie ihn herabsetzen, schimmert der Hintergrund durch, auch dort, wo das Bild nicht transparent ist. Spielen Sie auch hiermit ruhig einmal herum. Neben der hier verwendeten Methode existieren noch weitere -draw…-Methoden. Außerdem kann mit den -dissolve…-Methoden eine Zeichenoperation eingeleitet werden, die stets das auch von uns verwendete Source-Over-Compositing verwenden. Schließlich existiert ein Satz von -composite…-Methoden, der nicht mehr verwendet werden soll. Für älteren Code enthält die Dokumentation einen Hinweis auf entsprechende -draw…-Methoden.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 13« von der Webseite herunterladen. Bitte entfernen Sie wiederum den zusätzlichen Code aus -drawRect:.
81
SmartBooks
Objective-C und Cocoa – Band 2
Zeichnen in andere Ziele Bisher haben wir immer nur vom Zeichnen in ein View in einem Fenster gesprochen. Es gibt jedoch noch andere Ziele:
Ausgabe in Images Sie haben ja gelernt, dass Images unter anderem als Rastergraphiken abgelegt sein können. Auch der Bildschirminhalt ist nichts anderes als eine Rastergraphik. Also ist es ja naheliegend, dass man die Ausgabe statt auf der Rastergraphik Bildschirminhalt auf der Rastergraphik Image tätigt.
AUFGEPASST Das ist also der umgekehrte Fall zu vorhin: Dort haben wir ein Image in einen View gezeichnet. Jetzt zeichnen wir den Inhalt des Views in ein Image. Und dies ist in der Tat verblüffend einfach. Fügen Sie in CardStackView eine Actionmethode -createTiff: am Ende nach -drawRect: hinzu: - (IBAction)createTiff:(id)sender { NSSize imageSize = [self bounds].size; NSRect cardRect = NSInsetRect( [self bounds], 0.5, 0.5 ); NSImage* image = [[[NSImage alloc] initWithSize:imageSize] autorelease]; [image lockFocus]; NSFrameRect( cardRect ); [self drawCardFaceWithSuit:2 value:0 inRect:cardRect]; [image unlockFocus]; NSData* tiff = [image TIFFRepresentation]; NSString* path = @"~/Desktop/Bild.tiff"; path = [path stringByExpandingTildeInPath]; [tiff writeToFile:path atomically:YES]; }
Zunächst erzeugen wir uns eine Instanz von NSImage. Diese ist noch leer, hat also noch keine Repräsentation. Dann ergibt sich die magische Methode -lockFocus. Diese sucht zunächst nach einer Repräsentation, die für unseren Bildschirm passend ist, sollte ein Bild mehr als eine Repräsentation aufweisen. Man kann mit der Methode -lockFocusOnRepresentation: auch dafür sorgen, dass in eine bestimmte Repräsentation gezeichnet wird. In unserem Falle ist es aber ohnehin so, dass noch
82
Kapitel 1
Graphische Ausgabe
gar keine Repräsentation vorliegt. Daher wird von Cocoa eine erzeugt, die unserem Bildschirm entspricht. Die zweite Wirkung von -lockFocus ist, dass nunmehr die folgenden Graphikbefehle in dieses Image gezeichnet werden. Sind wir damit fertig, so nehmen wir einfach den Focus mittels –unlockFocus wieder weg. Ausgangs der Methode wird dann der Inhalt des Images auf die Festplatte geschrieben. Wenn Sie bei sich auf dem Desktop nach Bild.tiff suchen, werden Sie eine entsprechende Zeichnung mit unserer Spielkarte vorfinden. Machen Sie bitte die Actionmethode im Header bekannt. Wechseln Sie in den Interface-Builder und fügen Sie in das Fenster einen neuen Button ein, den Sie bitte mit der Actionmethode verbinden. Testen. Diesen Code werden wir später in ähnlicher Form noch benötigen. Wenn nämlich ein Drag & Drop durchgeführt wird, muss ein Bild erzeugt werden, welches den gezogenen Gegenstand zeigt.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 14« von der Webseite herunterladen. Entfernen Sie den Button wieder.
Fullscreen Eine weitere Möglichkeit, die gerne erfragt wird, wollen wir aber schon hier vorstellen: den Fullscreen-Mode, also gleichermaßen das Zeichnen auf den Bildschirm. Das geht auch schön schnell und einfach: Zunächst fügen wir im App-Delegate ein Outlet auf das Fenster hinzu. Außerdem publizieren wir zwei Actionmethoden: @interface Card_GameAppDelegate : NSObject { … } - (IBAction)enterFullScreenMode:(id)sender; - (IBAction)exitFullScreenMode:(id)sender; @end
Diese programmieren wir dann in Card_GameAppDelegate aus:
83
SmartBooks
Objective-C und Cocoa – Band 2
@synthesize window - (IBAction)enterFullScreenMode:(id)sender { NSScreen* screen = [NSScreen mainScreen]; [[window contentView] enterFullScreenMode:screen withOptions:nil]; } - (IBAction)exitFullScreenMode:(id)sender { [[window contentView] exitFullScreenModeWithOptions:nil]; }
Die Sache ist ganz einfach: Wir ermitteln einen Bildschirm. Der ist in Cocoa durch die Klasse NSScreen modelliert. Mit der Methode +mainScreen erhalten wir den Hauptbildschirm, das ist jener mit der Menüzeile. Dann sagen wir einem View – wir haben das Content-View des Fensters gewählt – mit -enterFullScreenMode:withOptions:, dass es den gesamten Bildschirm bedecken soll. Das View bemerkt davon ja nichts. Es werden »ganz normal« Frame und Bounds gesetzt. Um den Rest kümmert sich das System. (Die Methode existiert übrigens unter 10.4 noch nicht, was aber mittlerweile gleichgültig sein sollte.) Die Optionen können Sie in der Dokumentation nachlesen. Wie Sie sehen können, wird der Fullscreen-Mode wieder mit -exitFullScreenModeWithOptions: verlassen. Um das Ganze zu testen, müssen wir freilich noch in den Nib gehen und dort zwei Buttons ins Fenster legen. Diese verbinden Sie dann mit den beiden neuen Methoden. (Es ist übrigens keine gute Idee, dem Nutzer zum Verlassen des FullscreenModes einen Eintrag im Hauptmenü zur Verfügung zu stellen, da dieses im Fullscreen-Mode nicht existiert.)
HILFE Sie können das Projekt im aktuellen Zustand als »Card Game 15« von der Webseite herunterladen. Bitte entfernen Sie wieder die Buttons.
84
Kapitel 1
Graphische Ausgabe
Graphischer Kontext Ein weiteres Konzept, welches für die Graphikausgabe unter Cocoa wichtig ist, ist der graphische Kontext (graphische Umgebung, Graphics-Context). Er gehört zu den Dingen, die man nicht vermisst, wenn man sie nicht kennt, aber doch zu schätzen weiß, wenn man sie kennt.
Funktion des Kontextes Um die Aufgabe von Kontexten zu verstehen, schauen wir uns mal eine Stelle des bisherigen Codes an: [[NSColor redColor] set];
Hiermit wurde die Farbe für künftige Ausgabeoperationen gesetzt. Aber die anderen Zeichenobjekte wie Bezier-Pfade bemerkten davon ja nichts. Es handelt sich ja nicht um eine ihrer Eigenschaften wie etwa die Linienstärke, welche mit -setLine Width: (NSBezierPath) gesetzt wurde. Auch die einfachen C-Funktionen, die gar nicht über Eigenschaften verfügen, kannten die Farbe. Ebenfalls erfolgte die Ausgabe immer an der richtige Stelle: -drawRect: von TableBaizeView zeichnete in sein Rechteck, -drawRect: von CardStackView ebenfalls. Aber derselbe Code konnte auch in ein Image zeichnen. Bei der graphischen Ausgabe wurden zudem Umrechnungen vorgenommen. Es muss also irgendwen im Hintergrund geben, der dies alles für uns erledigt. Es ist eine Instanz von NSGraphicsContext. Dieser graphische Kontext wird für jeden View (oder für jedes Image, wenn wir dort hinein zeichnen, oder jedes Ausgabegerät, wenn wir drucken) angelegt und uns als Hintergrundrauschen geliefert. Man kann in -drawRect: – und selbstverständlich in dort ausgeführte Methoden – auf diesen Kontext mit +currentContext (NSGraphicsContext) darauf zugreifen. Und man kann selbst graphische Kontexte erzeugen. Dies funktioniert in der Manier eines Tellerstapels: Der bisherige Kontext wird auf einen Stapel gelegt und wir ändern den aktuellen. Möchten wir wieder zum alten Kontext zurückkehren, so holen wir ihn wieder vom Stapel. Es ist übrigens erforderlich, ebenso viele Teller wieder abzuräumen, wie man auf den Stapel gelegt hat. Probieren wir das an einem kleinen Beispiel in CardStackView aus: - (void)drawRect:(NSRect)rect { … // Farbe im aktuellen Kontext setzen und etwas ausgeben [[NSColor blackColor] set]; NSRectFill( NSMakeRect( 10.0, 10.0, 20.0, 20.0 ) );
85
SmartBooks
Objective-C und Cocoa – Band 2
// Kontext sichern [NSGraphicsContext saveGraphicsState]; // Farbe im neuen Kontext setzen und etwas ausgeben [[NSColor blueColor] set]; NSRectFill( NSMakeRect( 30.0, 10.0, 20.0, 20.0 ) ); // Alten Kontext zurückholen [NSGraphicsContext restoreGraphicsState]; // Und wieder etwas ausgeben: Schwarz oder blau? NSRectFill( NSMakeRect( 50.0, 10.0, 20.0, 20.0 ) ); }
Das letzte Rechteck wird wieder schwarz gezeichnet. Zwar wird diese Farbe nicht erneut gesetzt, aber die Änderung auf Blau erfolgte ja in einem mit +saveGraphicsState neu eröffneten Kontext, so dass sie mit +restoreGraphicsState wieder ihre Gültigkeit verliert. Übrig bleibt der letzte Status davor, und das war die Farbe schwarz. Entfernen Sie bitte wieder den Code.
Globale Zeichenattribute Über die Farbe hinaus existieren allerdings weitere Eigenschaften, die nicht unwichtig sind. Wie im Beispiel der Farbe werden diese allerdings häufig nicht als Eigenschaften des Kontextes gesetzt, sondern über jeweilige -set-Methoden der Eigenschaftsinstanz, also -set (NSColor) und nicht -setColor: (NSGraphicsContext). Man muss daher stets an zwei Stellen denken: die originären Eigenschaften des Kontextes und abgeleitete Eigenschaften. currentContext shouldAntialias = YES strokeColor: black
color black
setShouldAntialias: setStroke
Originäre und abgeleitete Eigenschaften werden unterschiedlich im Kontext gesetzt.
Composite-Operation Hiermit wird der Zeichenmodus festgelegt, wobei auch hier fast immer das richtige Source-Over Standard ist.
86
Kapitel 1
Graphische Ausgabe
Antialiasing Wir hatten ja eingangs des Kapitels lang und breit das Antialiasing erklärt. Man kann dies in einem Kontext abschalten: - (void)drawRect:(NSRect)rect { [[NSGraphicsContext currentContext] setShouldAntialias:NO]; … }
Starten Sie das Programm und beachten Sie die Änderung in der Ausgabe. Merken Sie es? Die ohne Antialiasing gerenderte Ausgabe sieht grottenschlecht aus. Schnell wieder die Zeile entfernen!
AUFGEPASST Da vor jedem Eintritt in -drawRect: ein neuer Kontext erzeugt wird, der nach der Rückkehr wieder beseitigt wird, müssen wir die Änderungen nicht für das System zurücknehmen. Bei der Farbe haben wir das bisher ja auch nicht gemacht. Image-Interpolation Die Image-Interpolation bestimmt, mit welcher Qualität (NSImageInterpolationDefault, NSImageInterPolationLow, NSImageInterpolationHigh, NSImageInterpolationNone) Bilder interpoliert werden. Vorsicht: Diese Eigenschaft wird nicht im Kontext gespeichert, so dass sie nicht mit -restoreGraphicsContext rückgängig gemacht werden kann. Daher muss man sich zuvor die letzte Image-Interpolation selbst merken. Pattern-Phase Verwendet man eine Farbe, die über ein Muster verfügt – siehe +colorWithPatternImage: (NSColor) –, so stellt sich die Frage, wie dieses Muster zum Fenster verschoben werden soll. Am einfachsten lässt sich das mit einem Beispiel erklären. Sorgen Sie dafür, dass sich ein Tiff mit der Bezeichnung »Bild« in der Source befindet. - (void)drawRect:(NSRect)rect { … [[NSColor whiteColor] setFill]; NSPoint offset = NSMakePoint( 20.0, 20.0 ); [[NSGraphicsContext currentContext] setPatternPhase:offset]; NSImage* image = [NSImage imageNamed:@"Bild"]; [[NSColor colorWithPatternImage:image] setFill];
87
SmartBooks
Objective-C und Cocoa – Band 2
[path fill]; … }
Der Kartenhintergrund besteht jetzt aus der im Bild gespeicherten Graphik. Je nachdem, wie Sie den Offset bestimmen, wird dabei das Bild verschoben. Entfernen Sie den neuen Code wieder. Schatten Ein weiteres Zeichenattribut ist der Schatten, der durch NSShadow modelliert ist. Versorgen wir unsere Karten mit einem solchen: - (void)drawRect:(NSRect)rect { … NSShadow* shadow = [[[NSShadow alloc] init] autorelease]; [shadow setShadowOffset:NSMakeSize( 10.0, 10.0 )]; [shadow setShadowColor:[NSColor blackColor]]; [shadow setShadowBlurRadius:5.0]; [shadow set]; [self drawCardFaceWithSuit:0 value:1 inRect:cardRect]; }
Bitte starten und anschauen! Der Offset ist die Verschiebung des Schattens zum Originalobjekt. Blur-Radius bezeichnet die Stärke der Unschärfe. Bitte entfernen Sie den Code wieder. Zeichensatz (Font) Es existieren vereinfachte Ausgabemethoden für Text wie -drawAtPoint:Attributes: in der Kategorie NSString( NSStringDrawing ). Diese benötigen einen Font, mit dem gezeichnet werden soll. Man kann diesen mittels -set analog zu Farben setzen. Im Kapitel über das Textsystem werden wir aber überlegene Methoden der Zeichenausgabe vorstellen. Clipping Wir hatten bereits erwähnt, dass Zeichenoperationen auf das Bounds-Rechteck des Views beschränkt sind, in dem gerade -drawRect: ausgeführt wird. Man nennt dies »Clipping«. Aus diesem Grunde ist es etwa im obigen Schattenbeispiel sinnlos, einen Kartenschatten zu zeichnen: Der läge ja außerhalb des Views. Wir würden lediglich den Schatten der Umrandung an der linken und unteren Seite sehen, da dieser ja auf die Karte fiel. Genau dieser Schatten interessiert uns aber nicht.
88
Kapitel 1
Graphische Ausgabe
Neben diesem Schattenbeispiel existiert ein zweiter klassischer Fall, in dem man außerhalb des Views zeichnen möchte: Der Focus-Ring, also diese blaue Umrandung um den View, der gerade für die Tastatureingabe selektiert ist. Allerdings ist das auch selten: Einen solchen Focus-Ring haben vor allem die gesondert besprochenen Controls, und diese kümmern sich bereits in der Basisimplementierung darum. Wie dem auch sei, mithilfe von NSBezierPath lässt sich ein Clipping im Kontext setzen: - (void)drawRect:(NSRect)rect { … CGFloat radius = NSWidth( cardRect ) * edgeRadius; [NSGraphicsContext saveGraphicsState]; NSRect focusRingRect = NSInsetRect( cardRect, -2.0, -2.0 ); NSSetFocusRingStyle( NSFocusRingOnly ); NSBezierPath* focusPath = [NSBezierPath bezierPathWithRoundedRect:focusRingRect xRadius:radius+2.0 yRadius:radius+2.0]; [focusPath setClip]; [focusPath stroke]; [NSGraphicsContext restoreGraphicsState]; … }
Bitte löschen Sie auch diesen Sourcetext wieder, nachdem Sie die Anwendung getestet haben. Transformationen Transformationen sind vereinfacht gesagt Umrechnungen von Koordinatensystemen. Zu jedem View hält der Kontext die aktuelle Transformation, um die BoundsKoordinaten des Views auf Monitorkoordinaten umzurechnen. Mit Transformationen beschäftigen wir uns im unmittelbaren Anschluss. Daher hier nur kurz die Anmerkung, dass man dies setzen kann: - (void)drawRect:(NSRect)rect { … [NSGraphicsContext saveGraphicsState]; NSAffineTransform* rotation; rotation = [NSAffineTransform transform]; [rotation rotateByDegrees:20.0];
89
SmartBooks
Objective-C und Cocoa – Band 2
[rotation concat]; [[NSColor blackColor] setStroke]; [path stroke]; [NSGraphicsContext restoreGraphicsState]; … }
Obiger Code dreht also für die Umrandung den Kontext. Lassen Sie die Zeilen zunächst noch einmal bestehen. Wieso eigentlich concat? Es existiert auch eine Methode -set, die jedoch alle bisherigen Transformationen durch unsere ersetzen würde. Da aber, wie bereits erwähnt, die Umrechnung der Koordinaten mit einem bereits bestehenden Kontext erfolgt, würden wir dies verlieren. Das Ergebnis ist nicht sehr ansehnlich, -concat indessen fügt unsere neue Transformation hinzu. Womit wir beim nächsten Abschnitt wären:
90
Kapitel 1
Graphische Ausgabe
Transformationen Wir haben bereits bei der Umrechnung von Bounds-Rechtecken in Frame-Rechtecken mit Verschiebungen, Vergrößerungen und Rotationen gearbeitet. Diese drei Umwandlungen des Koordinatensystems gehören zu den sogenannten affinen Transformationen. Und in der Tat zeichnet dies affine Transformationen aus: Sie rechnen von einem linearen Koordinatensystem in ein anderes um. Man kann daraus sozusagen eine »Mathematik der Umwandlung« bauen.
Grundlagen Der Witz an affinen Transformationen ist die deutliche Beschleunigung bei der Ausgabe. Dies findet seinen Grund darin, dass die wiederholte Anwendung von Transformationen in einer Transformation zusammengefasst werden kann. Nehmen wir zum Verständnis ein Beispiel, das stark vereinfacht ist: Ohne dass wir Rotationen und Skalierungen der beteiligten Views berücksichtigen, überlegen wir uns, was geschehen muss, wenn ein Punkt in unserem CardStacksView umgerechnet werden muss, weil er etwa Startpunkt einer Linie ist: Der Punkt ist durch seine Lage mit X- und Y-Koordinaten bestimmt. Dies bezieht sich jedoch auf sein Bounds-Rechteck. Dieses Bounds-Rechteck deckt sich mit seinem Frame-Rechteck, welches widerum für den Ursprung Koordinaten im Bounds-Rechteck des TableBaizeView hat. Um also dorthin zu gelangen, muss eine Verschiebung vorgenommen werden. Das hatten wir ja auch oben schon gesehen: P'.x = P.x + cardStackView.frame.origin.x P'.y = P.y + cardStackView.frame.origin.y
Wir kürzen die Schreibweise etwas ab: P'.x = P.x + cardStackView.x P'.y = P.y + cardStackView.y
Nun befindet sich dieser Punkt im TableBaizeView. Dieses ist wiederum Subview des Content-Views. Um also die Koordinaten in dem Content-View zu erhalten, muss erneut eine Verschiebung vorgenommen werden: P''.x = P'.x + tableBaizeView.x P''.y = P'.y + tableBaizeView.y
91
SmartBooks
Objective-C und Cocoa – Band 2
Dieser Contentview ist aber nun wieder ein Subview des »geheimen Hintergrundviews« des Fensters (Band I, S. 338 f.). Also noch eine Umrechnung, um die Koordinaten im System dieses Hintergrundviews zu haben: P'''.x = P''.x + contentView.x P'''.y = P''.y + contentView.y
Und dieses Hintergrundview hat wiederum ein Rechteck, welches die Lage auf dem Bildschirm angibt. Um also letztlich die Koordinaten auf dem Bildschirm anzugeben, ist eine letzte Umrechung erforderlich: P''''.x = P'''.x + hintergrundView.x P''''.y = P'''.y + hintergrundView.y
GRUNDLAGEN Es sei hier angemerkt, dass Cocoa in Wahrheit in der Regel in Zwischenpuffer schreibt, die dann als Ganzes auf dem Bildschirm eingeblendet werden. So wird etwa für ein Fenster eine eigene Bitmap angelegt, die dann auf dem Monitor eingeblendet wird. Mathematisch erfolgen hierbei jedoch dieselben Umrechnungen. Diese – in unserem Falle vier – Umrechnungen müssen für jeden Punkt, der ausgegeben wird, erledigt werden. Geben wir sehr viele Punkte aus, so wird dies rechenaufwendig. Wenn wir uns aber mal die obige Rechnungen anschauen, so können wir dies durch das Einsetzungsverfahren (ersetze P''' durch die Formel hierfür, ersetze dort drin P'' durch die Formel hierfür …) zusammenfassen: P''''.x = P.x + cardStackView.x + tableBaizeView.x + contentView.x + hintergrundView.x P''''.y = P.x + cardStackView.y + tableBaizeView.y + contentView.y + hintergrundView.y
und vereinfachen: offset.x = cardStackView.x + tableBaizeView.x + contentView.x + hintergrundView.x offset.y = cardStackView.y + tableBaizeView.y + contentView.y + hintergrundView.y P''''.x = P.x + offset.x P''''.y = P.x + offset.y
92
Kapitel 1
Graphische Ausgabe
Haben wir es mit weiteren Punkten zu tun, so müssen wir immer nur noch den Gesamtoffset addieren, also eine Addition statt vierer vornehmen. In der Realität ist das jedoch noch komplizierter, da ja auch Vergrößerungen und Drehungen hinzukommen. Zum einen kommt man dann nicht mehr mit einer einfachen Addition aus. Die Formel würde ellenlang – und wäre auch noch speziell für unseren Fall. Man benutzt daher zur Darstellung der affinen Transformationen sogenannte Vektoren und Matrizen. Vektoren sind die Bezeichnung unserer Punkte als Entfernung vom Ursprung. Eine Punkt { 5, 8 } ist also nichts anderes als ein Vektor vom Ursprung unseres Koordinatensystems zu diesem Punkt. Aus mathematisch-technischen Gründen wird jedoch eine weitere Komponente hinzugefügt, die stets 1 ist. Der Vektor lautet also eigentlich { 5, 8, 1 }. Da dies jedoch stets eine 1 ist, notieren wir die nicht mehr. Matrizen sind Vektoren von Vektoren, also eine zweidimensionale Anordnung von Zahlen. In unserem Falle mit zweidimensionalen Koordinaten hat eine solche Matrix die Größe 3 × 2. 5 8 1
2
9
8
3
2
5 Für zweidimensionale Kooridnatensysteme verwendet man Vektoren und 3×2-Matrizen.
Der Witz an diesen Matrizen ist, dass sich unsere drei affinen Transformationen als Matrix darstellen lassen. Und der weitere Witz ist, dass sich diese Matrizen wie oben unser einzelnes Offset durch Multiplikation verketten lassen. Man kann also die auf jeder Ebene der Viewhierachie notwendigen Umrechnungen Verschiebung, Vergrößerung und Drehung durch Verkettung zu einer Operation zusammenfassen. Und man kann diese Ebenen wieder durch Verkettung zusammenfassen, wie wir es oben mit dem Offset gemacht haben. Das Ergebnis ist, dass, gleichgültig, wie komplex die einzelnen Transformationen sind, am Ende eine einzige Matrix steht, die alle Operationen enthält. Damit muss lediglich jeder Punkt, den wir ausgeben, als Vektor mit dieser einen Gesamttransformation multipliziert werden. P'''' = P × M
93
SmartBooks
Objective-C und Cocoa – Band 2
Transformationen in Cocoa Dieses erledigt Cocoa bereits für uns. Also haben wir bei Betreten von -drawRect: bereits im Kontext eine Matrix gespeichert, die sämtliche notwendigen Transformationen enthält, um vom Bounds-Koordinatensystem zum Zielkoordinatensystem zu gelangen. Und wie oben gezeigt, können wir selbst Transformationen als Instanzen der Klasse NSAffineTransformation hinzufügen. Da dies einmal in eine neue Matrix eingerechnet werden, muss danach jeder Punkt nur mit dieser neuen Matrix multipliziert werden.
AUFGEPASST Wir werden später bei der Mauseingabe in unseren Views erkennen, dass es sich genau umgekehrt verhält: Es müssen die vom System gelieferten Koordinaten wieder in unser Bounds-Rechteck umgerechnet werden. Auf einen wichtigen Umstand müssen wir Sie allerdings noch hinweisen: Zwar lassen sich Transformationen durch Multiplikation verketten, jedoch ist diese Multiplikation entgegen unserer Gewöhnung nicht kommutativ: Man erhält in der Regel nicht dasselbe Ergebnis, wenn man die beiden Operanden vertauscht. Dies bedeutet, dass es auf die Reihenfolge der Transformationen ankommt. Und dies entspricht eigentlich doch wieder unserer Erwartung, wenn man das veranschaulicht:
Drehen × schieben ist etwas anderes als schieben × drehen.
94
Kapitel 1
Graphische Ausgabe
Allerdings ergibt sich ein auf den ersten Blick seltsames Verhalten, wenn man dies im Code ausprobiert: - (void)drawRect:(NSRect)rect { … [[NSGraphicsContext currentContext] saveGraphicsState]; NSAffineTransform* rotation; rotation = [NSAffineTransform transform]; [rotation rotateByDegrees:20.0]; NSAffineTransform* translation; translation = [NSAffineTransform transform]; [translation translateXBy:50.0 yBy:0]; [rotation concat]; [translation concat]; [[NSColor blackColor] setStroke]; … }
Führen Sie dies aus und vertauschen Sie dann die zwei Zeilen mit der Anwendung der Transformation (-concat). Sie werden bemerken, dass sich die Ausgabe genau entgegengesetzt wie hier beschrieben verhält. Dies hat einen einfachen Grund: Die Verkettung mittels -concat bedeutet nicht, dass unsere Transformationen als Letztes an die Transformationskette angehängt werden. Wäre dies der Fall, so wäre ja bereits die Umrechnung in das Zielkoordinatensystem (Bildschirm, Fenster) erfolgt, und etwa unsere Drehung würde sich auf dieses beziehen. Die Drehung würde also um den Ursprung des Bildschirms erfolgen, nicht um den Ursprung unseres Views. Dies wäre nicht nur nicht das erwartete Verhalten, sondern zudem davon abhängig, wo sich der View gerade befindet. Das Ergebnis der Operationen wäre also lokal nicht mehr vorhersehbar. Aus diesem Grunde werden eigene Transformationen E immer vor der Transformationskette C von Cocoa ausgeführt: C' = E × C Nimmt man mehrere Transformationen E1, E2 vor, so sind diese gleichsam »verkehrt herum«: C' = E2 × E1 × C 95
SmartBooks
Objective-C und Cocoa – Band 2
Dies gilt übrigens generell, wenn in Cocoa Transformationen angewendet werden. Nun löschen Sie aber den Code wieder. Transformationen lassen sich nicht nur auf den Kontext anwenden – sonst hätten wir sie ja auch vollständig dort besprochen –, sondern auch auf Bezier-Pfade. Und meist ist das der Königsweg, da sich so einzelne Figuren dauerhaft bearbeiten lassen. So liegt es auch hier: Wir wollen den noch etwas unförmig in der Mitte des Views beginnenden Text an seine Zielorte transformieren.
Der Pfad für die Beschriftung muss in zwei Kästchen transformiert werden.
Wir legen dabei eine Lage und Höhe für die Zielkästchen fest und überlassen dann die Breite einer verhältnismäßigen Skalierung. Bevor wir aber beginnen, sollten wir ein paar Konstanten in CardStackView.m für die Geometrie festlegen: static const float edgeRadius = 0.1; // Positionen in Kartengroesse static const NSPoint labelLocation = { 0.05, 0.9 }; static const CGFloat labelHeight = 0.08;
Als Nächstes schreiben wir uns zwei kleine Methoden, die jeweils das obere, linke bzw. untere, rechte Label zeichnen. Um uns die Sache einfacher zu machen, bauen wir uns dafür zunächst eine weitere Methode in der Kategorie BezierPathCardsAddition: - (void)transformToLocation:(NSPoint)location height:(CGFloat)height { NSAffineTransform* transform = [NSAffineTransform transform]; [transform translateXBy:location.x yBy:location.y]; CGFloat factor = height / NSHeight( [self bounds] );
96
Kapitel 1
Graphische Ausgabe
[transform scaleXBy:factor yBy:factor]; [self transformUsingAffineTransform:transform]; } @end
Hier ist die eigentlich thematische Arbeit drin: Es wird eine Transformation erzeugt, die zunächst den Pfad auf die notwendige Größe bringt und dann verschiebt. Moment, müsste das nicht umgekehrt sein? Richtig, müsste es: Erst vergrößern, dann verschieben. Aber es läuft ja verkehrt herum. Wenn Sie eine Transformation mit einer Transformation verketten, existieren übrigens die Methoden -appendTransform: und -prependTransform:, so dass Sie in beide Richtungen arbeiten können. Wir müssten dann hier aber zunächst die einzelnen Transformationen (Verschiebung, Skalierung) erzeugen, um diese explizit zu verketten. Wozu, wenn Sie so wie hier auch gleich ein weiteres Beispiel für die Reihenfolge der Anwendung erhalten? Bitte fügen Sie die Methode auch dem Header zu: @interface NSBezierPath( BezierPathCardsAddition ) - (void)appendBezierPathForSuit:(NSUInteger)suit; - (void)appendBezierPathForValue:(NSUInteger)index; - (void)transformToLocation:(NSPoint)location height:(CGFloat)height; @end
Kommen wir zur Anwendung, zunächst für das obere Label. In CardStackView setzen Sie vor unserer Methode -drawCardFaceWithSuit:value:inRect: eine neue: - (void)drawUpperLabelWithSuit:(NSUInteger)suit value:(NSUInteger)value inRect:(NSRect)rect { NSBezierPath* path; // Berechne das Label-Rect NSPoint location; location.x = NSMinX( rect ) + NSWidth( rect ) * labelLocation.x; location.y = NSMinY( rect ) + NSHeight( rect ) * labelLocation.y; CGFloat height = rect.size.height * labelHeight; // Farbsymbol erzeugen, anpassen und ausgeben
97
SmartBooks
Objective-C und Cocoa – Band 2
path = [NSBezierPath bezierPath]; [path moveToPoint:NSMakePoint( 0.0, 0.0)]; [path appendBezierPathForSuit:suit]; [path transformToLocation:location height:height]; [path fill]; location.x = NSMaxX( [path bounds] ) + 0.01 * NSWidth( rect ); // Wertesymbol erzeugen, anpassen und ausgeben path = [NSBezierPath bezierPath]; [path moveToPoint:NSMakePoint( 0.0, 0.0)]; [path appendBezierPathForValue:value]; [path transformToLocation:location height:height]; [path fill]; } - (void)drawCardFaceWithSuit:(NSUInteger)suit value:(NSUInteger)value inRect:(NSRect)rect
Das ist jetzt eigentlich mehr etwas Mathematik anstelle von Software-Entwicklung: Die Bemaßung des Rechteckes wird ermittelt und dann später für die Transformationen benutzt. Beachten Sie noch, dass vor der Erzeugung des Wertesymbols der Ort etwas nach rechts verschoben wird, damit dieser hinter dem Symbol für die Farbe steht. Für das untere Rechteck können Sie die obige Methode kopieren und dann nur noch ein bisschen ändern. Man mag nämlich zunächst glauben, dass eine Rotation um 180 Grad notwendig ist. Das ist auch richtig, nur lässt diese sich auch dadurch erzielen, dass man die Skalierung in beiden Achsenrichtungen negativ vornimmt. Wir haben daher hier nur die Stellen markiert, die nach dem Copy & Paste geändert werden müssen:
AUFGEPASST Es sei noch einmal darauf hingewiesen, dass die dabei entstehenden Rechtecke mit negativen Größenkomponenten nicht von allen Cocoa-Funktionen gemocht werden. Die Dokumentation zu den konkreten Funktionen verrät dies im Einzelnen.
98
Kapitel 1
Graphische Ausgabe
- (void)drawLowerLabelWithSuit:(NSUInteger)suit value:(NSUInteger)value inRect:(NSRect)rect { NSBezierPath* path; // Berechne das Label-Rect NSPoint location; location.x = NSMaxX( rect ) - NSWidth( rect ) * labelLocation.x; location.y = NSMaxY( rect ) - NSHeight( rect ) * labelLocation.y; CGFloat height = -1.0 * rect.size.height * labelHeight; // Farbsymbol erzeugen, anpassen und ausgeben … location.x = NSMinX( [path bounds] ) - 0.01 * NSWidth( rect ); // Wertesymbol erzeugen, anpassen und ausgeben … }
Die Hauptmethode zum Zeichnen der Kartenoberfläche reduziert sich enorm: - (void)drawCardFaceWithSuit:(NSUInteger)suit value:(NSUInteger)value inRect:(NSRect)rect { if( suit < 2 ) { [[NSColor redColor] set]; } else { [[NSColor blackColor] set]; } [self drawUpperLabelWithSuit:suit value:value inRect:rect]; [self drawLowerLabelWithSuit:suit value:value inRect:rect]; }
Testen Sie bitte das Programm. Die Beschriftungen oben links und unten rechts müssten jetzt erscheinen. Sie können freilich oben an den Konstanten noch herumspielen, wenn Ihnen Größe und Lage nicht zusagen.
99
SmartBooks
Objective-C und Cocoa – Band 2
Zwischenspurt: Strukturierung Da wir bisher zum Testen von Code ziemlich wild in dem View herumprogrammiert haben, müssen wir mal ein wenig aufräumen. Außerdem müssen wir uns noch darum kümmern, dass die Karten in den Views überhaupt ein festes Seitenverhältnis haben. Bisher bedeckt ja eine Karte den gesamten View. Es sollen indessen mehrere Karten mit dem richtigen Seitenverhältnis untereinander bzw. nebeneinander angezeigt werden: CardStackView heißt das Dingens ja. Dazu müssen wir freilich zunächst einmal die darzustellenden Karten kennen. Wir nehmen daher schon hier die Gelegenheit wahr, unserem CardStackView ein paar Instanzvariablen zu spendieren: @interface CardStackView : NSView { NSArray* cards; NSRectEdge alignment; } @property( copy ) NSArray* cards; @property NSRectEdge alignment;
cards soll später die auf dem Stapel abgelegten Karten aufnehmen, alignment enthält die Ausrichtung der Stapel, indem es die feste Seite angibt, von der aus der Kartenstapel wächst. Bei einem nach unten wachsenden Stapel also die obere Kante, bei einem nach rechts wachsenden Stapel die linke Kante usw. Bei NSRect Edge handelt es sich um einen von Cocoa definierten Enum-Typen, der die Werte NSMinXEdge (linke Kante), NSMaxXEdge (rechte Kante), NSMinYEdge (untere Kante), NSMaxYEdge (obere Kante) aufnehmen kann.
AUFGEPASST Es ist bereits in Band I erwähnt, vielleicht hatten Sie es aber auch selbst schon bemerkt: Zeigt ein View mehrere Einzelobjekte an, etwa ein Tableview, ein Outlineview oder unser CardsStackView, so ist das Setzen der Elemente über Accessoren eher nachteilig. Das führt nämlich dazu, dass sämtliche Daten gegebenenfalls besorgt und gesetzt werden müssen. Im Kapitel »Datenflusskontrolle« lernen Sie mit Data-Sources bzw. eigenen Bindings elegantere Möglichkeiten kennen. Zunächst definieren wir uns eingangs CardStackView.m eine (modullokale) Konstante, die das Seitenverhältnis angibt:
100
Kapitel 1
Graphische Ausgabe
// Kartenbemassung static const float cardSizeRatio = 1.6; static const float edgeRadius = 0.1; // Positionen in Kartengroesse
Die Accessoren für die oben genannten Eigenschaften lassen wir uns synthetisieren: @implementation CardStackView @synthesize cards, alignment;
und müssen dann natürlich Initialisierung und Dekonstruktion in -initWithFrame: (dies ist der Designated-Initializer) bzw. -dealloc vornehmen. Damit wir überhaupt etwas sehen, befüllen wir auch unseren View gleich mit ein paar Karten: - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { self.alignment = NSMaxYEdge; NSDictionary* card; NSMutableArray* initCards = [NSMutableArray array]; card = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithUnsignedInteger:1], @"suit", [NSNumber numberWithUnsignedInteger:1], @"value", [NSNumber numberWithBool:YES], @"disclosed", nil]; [initCards addObject:card]; card = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithUnsignedInteger:2], @"suit", [NSNumber numberWithUnsignedInteger:7], @"value", [NSNumber numberWithBool:YES], @"disclosed", nil]; [initCards addObject:card]; card = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithUnsignedInteger:2], @"suit", [NSNumber numberWithUnsignedInteger:7], @"value", [NSNumber numberWithBool:YES], @"disclosed", nil]; [initCards addObject:card]; self.cards = initCards; self.alignment = NSMaxXEdge;
101
SmartBooks
Objective-C und Cocoa – Band 2
} return self; } - (void) dealloc { self.cards = nil; self.alignment = 0; [super dealloc]; }
Außerdem fügen Sie bitte – um etwas aufzuräumen – nach -drawCardFaceWith Suit:value:inRect: eine Methode zum Zeichnen des Hintergrundes ein. Sie können hier aus -drawRect: mit Copy & Paste arbeiten. Die Methode muss freilich insgesamt eingefügt werden: - (void)drawCardBorderInRect:(NSRect)rect { NSBezierPath* path; CGFloat radius = NSWidth( rect ) * edgeRadius; path = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:radius yRadius:radius]; [path setLineWidth:1.0]; [[NSColor whiteColor] setFill]; [path fill]; [[NSColor blackColor] setStroke]; [path stroke]; } - (void)drawRect:(NSRect)rect {
Um den Code übersichtlich zu halten, schreiben wir uns ferner hinter der neuen -drawCardBorderInRect: eine weitere Methode, die aus dem Alignment und den Bounds das Rechteck für eine Karte errechnet. Es sei übrigens angemerkt, dass diese Methode über die Parameterliste einen Wert jammed zurückliefert, der bestimmt, ob die Karten zusammengestaucht werden mussten, um in den View zu passen. Wir werden diesen im weiteren Verlauf des Kapitels noch benötigen. Weil 102
Kapitel 1
Graphische Ausgabe
wir dies später auch im TableBaizeView benötigen, erzeugen wir uns zudem zwei weitere Methoden: + (CGFloat)cardHeightForWidth:(CGFloat)width { return width * cardSizeRatio; } + (CGFloat)cardWidthForHeight:(CGFloat)height { return height / cardSizeRatio; } - (NSRect)rectForCardWithIndex:(NSUInteger)index count:(NSUInteger)count getIsJammed:(BOOL*)jammed { CGFloat xStep; CGFloat yStep; NSRect rect = [self bounds]; // Startwert und Schrittweite nach Ausrichtungen setzen switch( self.alignment ) { case NSMinXEdge: // linksbuendig rect.size.width = [[self class] cardWidthForHeight:NSHeight( rect )]; rect.origin.x = 0.5; xStep = NSWidth( rect ) * +stepSize.width; yStep = 0.0; break; case NSMaxXEdge: // rechtbuendig rect.size.width = [[self class] cardWidthForHeight:NSHeight( rect )]; rect.origin.x = NSMaxX( [self bounds] ) - NSWidth( rect ) - 0.5; xStep = NSWidth( rect ) * -stepSize.width; yStep = 0.0; break; case NSMinYEdge: // untenbuendig (hier nicht verwendet) rect.size.height
103
SmartBooks
Objective-C und Cocoa – Band 2
= [[self class] cardHeightForWidth:NSWidth( rect )]; rect.origin.y = 0.5; xStep = 0.0; yStep = NSHeight( rect ) * +stepSize.height; break; case NSMaxYEdge: // obenbuendig rect.size.height = [[self class] cardHeightForWidth:NSWidth( rect )]; rect.origin.y = NSMaxY( [self bounds] ) - NSHeight( rect ) - 0.5; xStep = 0.0; yStep = NSHeight( rect ) * -stepSize.height; break; default: break; } // Ueberlaeufe abfangen *jammed = NO; CGFloat maxStep; count--; if( count > 0 ) { maxStep = (NSHeight([self bounds])-NSHeight(rect)) / count; if( yStep < 0.0 ) { if( -yStep > maxStep ) { yStep = -maxStep; *jammed = YES; } } else { if( yStep > maxStep ) { yStep = maxStep; *jammed = YES; } } maxStep = (NSWidth([self bounds])-NSWidth(rect)) / count; if( xStep < 0.0 ) { if( -xStep > maxStep ) { xStep = -maxStep;
104
Kapitel 1
Graphische Ausgabe
*jammed = YES; } } else { if( xStep > maxStep ) { xStep = +maxStep; *jammed = YES; } } } // Verschiebe Karte um Index rect = NSOffsetRect( rect, xStep * index, yStep * index ); return rect; } - (void)drawRect:(NSRect)rect
Die hier verwendeten Konstanten für die Schrittweite fügen Sie bitte wieder am Anfang der Datei hinzu: // Kartenbemassung static const float cardSizeRatio = 1.6; static const float edgeRadius = 0.1; static const NSSize stepSize = { 0.21, 0.11 };
Entsprechend müssen wir unser -drawRect: anpassen: - (void)drawRect:(NSRect)rect { BOOL jammed; // Hier nicht von Interesse NSUInteger suit; NSUInteger value; NSUInteger index;
NSUInteger count = [self.cards count]; for( index = 0; index < count; index++ ) { id card = [self.cards objectAtIndex:index]; NSRect cardRect = [self rectForCardWithIndex:index count:count getIsJammed:&jammed]; [self drawCardBorderInRect:cardRect];
105
SmartBooks
Objective-C und Cocoa – Band 2
suit = [[card valueForKey:@"suit"] integerValue]; value = [[card valueForKey:@"value"] integerValue]; [self drawCardFaceWithSuit:suit value:value inRect:cardRect]; } }
Schließlich machen wir die beiden Klassenmethoden im Header zur späteren Benutzung in TableBaizeView im Header von CardStackView bekannt: @property NSRectEdge alignment; + (CGFloat)cardHeightForWidth:(CGFloat)width; + (CGFloat)cardWidthForHeight:(CGFloat)height;
Gut, damit sind wir hier erst einmal fertig. War ja auch anstrengend genug. Begeben Sie sich jetzt bitte in TableBaizeView.m. Hier werden wir jetzt die einzelnen Subviews erzeugen und hinzufügen. Außerdem werden wir uns darum kümmern, unser ganz eigenes Sizing einzubauen. Sie sollten das CardStackView im Interface Builder in die Breite ziehen, da standardmäßig die Karten horizontal aufgefächert werden.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 16« von der Webseite herunterladen.
106
Kapitel 1
Graphische Ausgabe
View-Hierarchien Zuweilen existieren feste Hierarchien von Views. Sie kennen das etwa von dem Scrollview, welcher ganz bestimmte Subviews hat (Band I, S. 404). Auch bei unserem Kartenspiel ist die Hierarchie fest. Es ist kaum sinnvoll, im Interface Builder Subviews hinzuzufügen, da diese ja nicht mit den Regeln des Spiels konform gingen. Es ist in solchen Fällen eine gute Idee, die View-Struktur vom Superview – bei uns das TableBaizeView – erzeugen zu lassen. …
…
…
Zehn Views mit vertikaler Ausrichtung und zwei Views mit horizontaler Ausrichtung bilden den Kartentisch.
Und wir werden uns um ein zweites Problem kümmern müssen: Bei einer Änderung der Fenstergröße müssen die Größen der einzelnen Stapel angepasst werden. Das haben wir bisher über das Autoresizing von Cocoa gemacht. Aber das reicht hier nicht aus, wie man an einer einfachen Überlegung sieht: Trivial ergibt sich die Breite der oberen Stapel aus der Breite des Fensters (genauer: aus der Breite des TableBaizeViews, dessen Breite sich wiederum aus dem Fenster ergibt). Da wir die Seitenverhältnisse der Karten fix halten wollen, ergibt sich damit die Höhe der unteren Stapel mittelbar aus der Breite des Fensters. Eine derartige Abhängigkeit lässt sich weder im Interface Builder noch durch Code einstellen, weil sie in Cocoa nicht implementiert ist. Hier müssen wir uns also selbst dran setzen. 107
SmartBooks
Objective-C und Cocoa – Band 2
Erzeugen im Code Sie haben bereits in Band 1 (S. 536 ff.) eine Methode kennengelernt, mit der sich Views zur Laufzeit dynamisch laden und einblenden lassen. Hierzu wurden Nibs mit Views und View-Controller verwendet. Dies ist auch weiterhin die richtige Vorgehensweise, wenn mehrere vorgefertigte, jedoch unabhängige Views zur Laufzeit erzeugt werden sollen. Hier haben wir es jedoch mit einem anderen Problem zu tun: Wir haben eine durch die Funktionalität vorgefertigte Struktur von Views, die wir bereits mit Scrollviews verglichen haben. Man mag daran denken, dass diese einstellbar ist und sich daher wieder auflöst. Allerdings gilt das ebenfalls für Tablebaizeviews, da man etwa im Interface Builder die Scroller beeinflussen kann. Und das sind auch Subviews. Vielmehr verhält es sich so, dass das äußere Objekt, also das Tablebaizeview, die Einstellungen entgegennimmt und daraufhin seine innere Struktur bildet. Nicht die Subviews selbst sind also Eigenschaften des übergeordneten Views. Vielmehr organisiert sich die Viewstruktur anhand anderer Eigenschaften. In solchen Fällen sollte diese Kapselung nicht durchbrochen werden, indem der Programmierer selbst Views hinzufügt, sei es im Code oder im Interface Builder. Vielmehr muss die äußere Klasse ihren inneren Aufbau selbst besorgen.
GRUNDLAGEN Auch wenn wir das hier aus Platzgründen nicht durchprogrammieren. Aber für unser Spiel existieren verschiedene Varianten mit unterschiedlicher Anzahl von Karten. Diese unterschiedliche Anzahl von Karten kann zu einer unterschiedlichen Anzahl an die durch unsere Views dargestellten Stapel führen. Wir würden daher im äußeren Tablebaizeview lediglich ein API zum Setzen der Spielversion (-setNumberOfCardSets: oder ähnlich) anbieten. Der Tablebaizeview muss darauf reagieren, indem er eine ausreichende Anzahl an Subviews erzeugt: Sein (inneres) Implementierungsproblem. Anders wäre es vermutlich, wenn wir Viewklassen programmieren, die völlig unterschiedliche Spiele darstellen können. Hier ist es nicht mehr sinnvoll, sich ein dann hochkomplexes API für alle Varianten auszudenken. Daher wäre es sinnvoll, auch die graphische Gestaltung offenzulegen und im Interface Builder oder einer Controllerinstanz die einzelnen Subviews zu erzeugen. Wie immer, wenn man Elemente eines Views initialisiert, stellt sich die Frage des richtigen Ortes. Wie bereits im ersten Band auf S. 352 erläutert, erfolgt die interne Initialisierung einer Instanz im Nib in -init… während alle Aufgaben, die mit anderen Elementen zusammenhängen, in -awakeFromNib: erledigt werden. Da wir es hier ja mit 108
Kapitel 1
Graphische Ausgabe
einer inneren Kapselung zu tun haben, müssen wir unsere Subviews in -init… erzeugen. Zunächst öffnen wir hierzu TableBaizeView.h und fügen Instanzvariablen hinzu: @class CardStackView; @interface TableBaizeView : NSView { NSMutableArray* playerViews; CardStackView* depotView; CardStackView* targetView; } @end
Auf Propertys haben wir hier verzichtet, da diese Views ohnehin nicht von außen gesetzt werden sollen. Öffnen Sie TableBaizeView.m und suchen Sie dort den Designated-Initializer -initWithFrame:. Views sind auch nur Instanzen und werden daher entsprechend erzeugt: #import "TableBaizeView.h" #import "CardStackView.h" - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { // Zehn obere Stapel CardStackView* view; playerViews = [[NSMutableArray alloc] init]; NSUInteger index; for( index = 0; index < 10; index++ ) { view = [[[CardStackView alloc] initWithFrame:NSZeroRect] autorelease]; [view setAlignment:NSMaxYEdge]; [self addSubview:view]; [playerViews addObject:view]; } // Depot view = [[[CardStackView alloc] initWithFrame:NSZeroRect] autorelease]; [view setAlignment:NSMinXEdge]; [self addSubview:view]; depotView = view;
109
SmartBooks
Objective-C und Cocoa – Band 2
// Ablage view = [[[CardStackView alloc] initWithFrame:NSZeroRect] autorelease]; [view setAlignment:NSMaxXEdge]; [self addSubview:view]; targetView = view; } return self; }
AUFGEPASST Speicherverwaltung? Die Views werden als Subviews angemeldet und daher von unserem View gehalten. Es ist daher nicht notwendig, ihnen erneut ein retain zu schicken. Es handelt sich also um weiche Beziehungen. Dieses Verfahren, keine Setter im -init… und -dealloc zu verwenden, wird durchaus angewendet. Es ist dann ganz okay, wenn, wie hier, später niemals mehr eine Änderung erfolgt: Alles intern. Das Array für die zehn oberen Stapel wird ebenfalls so behandelt. Es darf allerdings kein autorelease erhalten. Aufgeräumt wird im -dealloc: - (void) dealloc { [playerViews release]; playerViews = nil; depotView = nil; targetView = nil; [super dealloc]; }
Entfernen Sie das bisherige CardStackView im Interface Builder.
Sizing Wenn Sie das Projekt jetzt starten, werden Sie sehen, dass Sie nichts sehen. Dies ist ja auch kein Wunder: In der Methode werden alle Subviews mit einem FrameRechteck von { 0, 0, 0, 0 } erzeugt, was nicht sehr beeindruckend ist. Da wir jedoch die Berechnung der Größen auch bei einem Resizing benötigen, fügen wir ganz am Anfang der Implementierung eine neue Methode ein: 110
Kapitel 1
Graphische Ausgabe
@implementation TableBaizeView - (void)resizeSubviews { NSSize cardSize; NSRect rect; NSRect bounds = [self bounds]; // Kartenausmasse bestimmen: Die Zweichenraeume sollen jeweils // 10 % der Breite einnehmen. Da neun Zwischenraeume und zwei // seitliche existieren, entspricht das 1,1 Karten, insgesamt // haben wir also 11,1 Kartenbreiten CGFloat border = bounds.size.width / 111; cardSize.width = bounds.size.width / 11.1; cardSize.height = [[CardStackView class] cardHeightForWidth:cardSize.width]; // Hieraus ergibt sich das Rechteck, in dem die Karten liegen bounds = NSInsetRect( bounds, border, border ); // Die einer Karte ist zugleich die Hoehe der unteren waage// rechten Stapel. Sie belgen jeweils die Hälte des Platzes rect.origin = bounds.origin; rect.size.width = NSWidth( bounds ) / 2; rect.size.height = cardSize.height; [depotView setFrame:rect]; rect.origin.x = NSMaxX( rect ); [targetView setFrame:rect]; // Fuer die oberen Karten ergibt sich daraus das Rechteck rect.origin.x = NSMinX( bounds ); rect.origin.y = NSMaxY( rect ) + border; rect.size.width = cardSize.width; rect.size.height = NSHeight( bounds) - NSMinY( rect ); // die einzelnen Views einfach nacheinander setzen for( CardStackView* playerView in playerViews ) { [playerView setFrame:rect]; rect.origin.x += rect.size.width + border; } }
111
SmartBooks
Objective-C und Cocoa – Band 2
Diese rufen wir dann in -initWithFrame: auf: - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { … targetView = view; [self resizeSubviews]; } return self; }
Überprüfen Sie jetzt, ob die einzelnen Views erscheinen. Sie können testweise auch im CardStackView am Ende von -drawRect: mit NSFrameRect() mal die Bounds zeichnen lassen. Bauen wir noch das automatische Resizing der Subviews ein: Wenn die Größe eines Views geändert wird, so wird automatisch die Methode -resizeSubviewsWith OldSize: aufgerufen. Diese überschreiben wir also: - (void)resizeSubviewsWithOldSize:(NSSize)oldSize { [self resizeSubviews]; } - (void)drawRect:(NSRect)rect {
Jetzt klappt es auch mit dem Resizing.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 17« von der Webseite herunterladen.
112
Kapitel 1
Graphische Ausgabe
Pixelgenaues Zeichnen Sie sehen im letzten Abschnitt schon, dass bei der Bestimmung der Frame-Rechtecke für die Subviews »krumme« Werte entstehen können. Dies führt dazu, dass die Umrandungen nicht mehr auf Pixelgrenzen gezeichnet werden. Während dies grundsätzlich kein Problem ist, empfindet man es gerade bei Umrandungen zuweilen als störend. Um das zu vermeiden, müssen die Koordinaten also wieder auf Pixeln liegen. Bis Mac OS X 10.4 durfte man davon ausgehen, dass eine Einheit des Koordinatensystems (Punkt) exakt den Pixeln entsprach. Wenn man also keine Vergrößerung in seinem View hatte, so musste man nur Haarlinien mit Koordinaten zeichnen, die auf Komma 5 endeten. Eine andere Möglichkeit bestand – und besteht – darin, dass Antialiasing auszuschalten. Nur würden dann auch unsere runden Kartenecken nicht mehr sauber gezeichnet. Hier wollen wir ja Antialiasing. Und wenn gezoomt wird, funktioniert der »Komma-5-Trick« ohnehin nicht mehr. Um alle diese Probleme zu lösen, existiert mit 10.5 die Möglichkeit, die Zeichenkoordinaten auf Pixel zu runden.
AUFGEPASST Wir gehen hierbei davon aus, dass wir es mit einem Gerät zu tun haben, welches die Pixel äquidistant anordnet, bei dem also die Entfernung von einer Pixelmitte zu der des Nachbarn stets konstant ist. Diese – durchaus haltbare – Annahme führt dazu, dass man davon ausgehen darf, dass die Addition einer ganzzahligen Weite bzw. Höhe von der Mitte eines Pixels wieder in der Mitte eines Pixels landet. Wenn diese Annahme nicht getroffen werden kann, ändert dies aber auch nichts an dem hier erläuterten System: Es müssen dann einfach viel mehr Umwandlungen vorgenommen werden. Zunächst ist es eine gute Idee, dafür zu sorgen, dass die einzelnen Subviews auf ganzzahligen Koordinaten liegen. Das ist mindestens als Basis für das weitere Vorgehen sehr sinnvoll. Hierzu ändern wir unser -resizeSubviews: - (void)resizeSubviews { NSSize cardSize; NSRect rect; NSRect bounds = [self bounds];
113
SmartBooks
Objective-C und Cocoa – Band 2
// Kartenausmasse bestimmen: Die Zweichenraeume sollen jeweils // 10 % der Breite einnehmen. Da neun Zwischenraeume und zwei // seitliche existieren, entspricht das 1,1 Karten, insgesamt // haben wir also 11,1 Kartenbreiten CGFloat border = bounds.size.width / 111; cardSize.width = bounds.size.width / 11.1; cardSize.height = [[CardStackView class] cardHeightForWidth:cardSize.width]; // Hieraus ergibt sich das Rechteck, in dem die Karten liegen bounds = NSInsetRect( bounds, border, border ); // Die einer Karte ist zugleich die Hoehe der unteren waage// rechten Stapel. Sie belgen jeweils die Hälte des Platzes rect.origin = bounds.origin; rect.size.width = NSWidth( bounds ) / 2; rect.size.height = cardSize.height; NSLog( @"bounds aligned: %@", NSStringFromRect( rect ) ); rect = [self centerScanRect:rect]; NSLog( @"device aligned: %@", NSStringFromRect( rect ) ); [depotView setFrame:rect]; rect.origin.x = NSMaxX( rect ); rect = [self centerScanRect:rect]; [targetView setFrame:rect]; // Fuer die oberen Karten ergibt sich daraus das Rechteck rect.origin.x = NSMinX( bounds ); rect.origin.y = NSMaxY( rect ) + border; rect.size.width = cardSize.width; rect.size.height = NSHeight( bounds) - NSMinY( rect ); // die einzelnen Views einfach nacheinander setzen for( CardStackView* playerView in playerViews ) { rect = [self centerScanRect:rect]; [playerView setFrame:rect]; rect.origin.x += rect.size.width + border; } }
114
Kapitel 1
Graphische Ausgabe
Sie sehen also, dass wir hier einfach -centerScanRect: verwenden, um das Rechteck auf Pixelkoordinaten (also gerätebezogen) auszurichten. Der Name dieser Methode ist etwas missverständlich: Die resultierenden Koordinaten liegen gerade nicht in der Mitte des Pixels, sondern auf den Kanten. Wir erhalten also glatte Werte im Geräte-Koordinatensystem zurück. Sie können sich das so vorstellen, dass ja auf einer konkreten Hardware die einzelnen Pixel ganzzahlig sind und daher wieder traditionell Flächen bezeichnen. Aber das ist genau das, was wir hier wollen. Als Nächstes ist zu bedenken, dass -centerScanRect: ja irgendwie unsere Koordinaten verändern muss. Wenn Sie die Subviews derart angeordnet haben, dass diese sich berühren, ist es daher angebracht, die Berührungspunkte umzurechnen, damit sich die Views nicht überlappen. Den vorgesehenen Methodensatz besprechen wir gleich noch. Bei uns liegt es aber hier so, dass zwischen den Subviews ausreichend Platz ist, so dass wir leichte Vergrößerungen der einzelnen Views hinnehmen können, ohne uns der Gefahr der Überlappung auszsetzen. Um die Umrechnung auszuprobieren, suchen Sie bitte mit Spotlight die Anwendung Quartz Debug und starten diese. Sie können dann dort mit dem Menübefehl Window | UI Resolution ein Fenster öffnen, in dem Sie einen Umrechnungsfaktor eingeben können. Wird danach ein neues Programm gestartet, wird ihm eine entsprechende Umrechnung zwischen virtuellen Koordinaten und Gerätekoordinaten vorgegaukelt. (Übrigens bei allen Programmen, was lustig aussehen kann.) Stellen Sie hier 1,25 ein und starten Sie unser Programm erneut. Ja, das sieht noch nicht perfekt aus, aber Sie können im Log die Umrechnung erkennen. Beachten Sie hierbei, dass die zurückgelieferten Koordinaten im Bounds-Koordinatensystem natürlich nicht glatt sind, da wir ja einen gebrochenen Umrechnungsfaktor haben. Beenden Sie das Programm wieder. Wir müssen uns noch um zwei Dinge kümmern: Zum einen hatten wir ja bisher eine Linienweite von 1 verwendet. Auch diese Angabe bezieht sich – wie alle – auf unser Bounds-Koordinatensystem. Bei einem Umrechnungsfaktor von 1,25 bedeutet dies also, dass 1,25 Pixel überdeckt werden sollen, was wiederum zu »verwischten« Linien führt. Wie aber bereits erwähnt, kann man als Breite 0 angeben, was von Cocoa automatisch als kleinste Breite des Gerätes (bei einem Bildschirm 1 Pixel) verstanden wird. Außerdem stimmt dann natürlich nicht mehr die Berechnung des Randes mit einer Verschiebung von 0,5 Pixeln im Bounds-Koordinatensystem. Fangen wir also an, den Code anzupassen. Kopieren Sie sich eine weitere Version von -rectForCardWithIndex:count:getIsJammed: unter die bisherige Fassung und benennen Sie diese bitte in
115
SmartBooks
Objective-C und Cocoa – Band 2
- (NSRect)printRectForCardWithIndex:(NSUInteger)index count:(NSUInteger)count getIsJammed:(BOOL*)jammed { … }
um. Hintergrund ist, dass wir jetzt das Kartenrechteck pixelgenau berechnen lassen. Für den Druck ist das jedoch wenig sinnvoll, da wir hier häufig gar nicht wissen, auf welchem Ausgabesystem der Ausdruck erfolgt: Kein bekanntes Ausgabesystem bedeutet aber, dass wir keine bekannte Rasterung haben. Wir speichern uns also einfach die bisherige Fassung für spätere Zwecke. In der ursprünglichen Fassung, also ohne print am Anfang, nehmen wir jetzt ein paar Änderungen vor: - (NSRect)rectForCardWithIndex:(NSUInteger)index count:(NSUInteger)count getIsJammed:(BOOL*)jammed { … // Startwert und Schrittweite nach Ausrichtungen setzen switch( self.alignment ) { case NSMinXEdge: // linksbuendig rect.size.width = [[self class] cardWidthForHeight:NSHeight( rect )]; rect.origin.x = 0.0; // Kein Inset mehr … case NSMaxXEdge: // rechtbuendig rect.size.width = [[self class] cardWidthForHeight:NSHeight( rect )]; rect.origin.x = NSMaxX( [self bounds] ) - NSWidth( rect ); … case NSMinYEdge: // untenbuendig (hier nicht verwendet) rect.size.height = [[self class] cardHeightForWidth:NSWidth( rect )]; rect.origin.y = 0.0; … case NSMaxYEdge: // obenbuendig rect.size.height = [[self class] cardHeightForWidth:NSWidth( rect )];
116
Kapitel 1
Graphische Ausgabe
NSLog( @"%f", NSHeight( rect ) ); rect.origin.y = NSMaxY( [self bounds] ) - NSHeight( rect ); … default: break; } // Ueberlaeufe abfangen … // Verschiebe Karte um Index rect = NSOffsetRect( rect, xStep * index, yStep * index ); // Pixelmaessig ausrichten rect = [self convertRectToBase:rect]; rect.origin.x = ceilf( rect.origin.x ) + 0.5; rect.origin.y = ceilf( rect.origin.y ) + 0.5; rect.size.width =floorf( rect.size.width ) - 2.0; rect.size.height =floorf( rect.size.height ) - 2.0; rect = [self convertRectFromBase:rect]; return rect; }
Sie sehen also, dass im Wesentlichen die pixelgenaue Verschiebung nunmehr nicht im Bounds-Koordinatensystem, sondern im Gerätekoordinatensystem vorgenommen wird. Neben den hier vorgestellten Methoden -convertRectToBase: existieren natürlich noch -convertSizeToBase: und -convertPointToBase:. Zu jeder Methode gibt es eine entsprechende Rückrechenmethode mit From anstelle von To im Namen. Im Code ist eine Anpassung des Vorzeichens erforderlich, da wir hier ja mit negativen Größen arbeiten. Die Konvertierungsroutinen »reparieren« das, so dass man positive Werte zurückbekommt. Das Vorzeichen wird also wieder restauriert. Hier müssen wir natürlich noch auf ein paar C-Funktionen eingehen: Mit den Funktionen floorf() (von engl. floor = dt. Boden, Untergrenze) wird der übergebene Wert abgerundet, wobei mit »Abrunden« der größte ganzzahlige Wert gemeint ist, der nicht größer ist als der übergebene Wert. Aus -3.2 wird also -4.0, nicht - 3.0, weil -3.0 größer als -3.2 wäre. ceilf() ist dasselbe in Grün fürs Aufrunden. (engl. ceiling = dt. (Zimmer-)Decke, Obergrenze). 117
SmartBooks
Objective-C und Cocoa – Band 2
Außerdem muss die Linienbreite jetzt 0.0 betragen: - (void)drawCardBorderInRect:(NSRect)rect { … [path setLineWidth:0.0]; … }
HILFE Sie können das Projekt in diesem Zustand als »Card Game 18« von der Webseite herunterladen.
118
Kapitel 1
Graphische Ausgabe
Zeichenanforderung Jetzt haben wir jede Menge Inhalte auf den Bildschirm gebracht. Wir hatten Ihnen jedoch gesagt, dass dies erst dann geschieht, wenn jemand Bereiche des Views als ungültig markiert. Wieso funktionierte das bisher also schon? Der Grund liegt darin, dass wir nur neu zeichneten, wenn wir die Größe eines Views veränderten. Hierfür wird allerdings unmittelbar von Cocoa schon eine Aufforderung zum (kompletten) Neuzeichnen geschickt. Wir mussten uns darum nicht kümmern. Aktuell wird das erst, wenn wir Änderungen in unserem Kartenstapel vornehmen. Hiervon weiß Cocoa selbst nichts, so dass wir eine Anforderung zum Neuzeichnen schicken müssen. Um zu verstehen, was genau passiert, müssen wir nun hinter die Kulissen schauen.
AUFGEPASST Dieses Thema berührt eigentlich gleich drei Bereiche: Zum einen ist die Aufforderung zum Neuzeichnen eine Nachricht, die im System verteilt werden muss. Das Thema hätte also auch gut ins nächste Kapitel gepasst. Nur liegt es auf der Hand, dass die Zuordnung von Nachricht zum View beim Neuzeichnen recht überschaubar ist: Die Nachricht erhält jenes View, für dass das Neuzeichnen angefordert wurde. Außerdem existiert ein Bezug zum dritten Kapitel, da wir ja neu zeichnen müssen, wenn jemand anderes – in der Regel ein Controller – Daten bei uns änderte. Aber auch hier ist es so, dass die eigentliche Anforderung vom View erzeugt wird. Warum das so ist, erfahren Sie sofort: Neuzeichnen als Entscheidung des Views Bereits in Band 1 gab es die Notwendigkeit, einen View neu zeichnen zu lassen. Dies betraf vor allem unser Tableview, und zwar dann, wenn wir neue Daten dafür hatten und diese nun angezeigt werden mussten. Sie teilten aber so dem Tableview nicht mit, dass er sich neu zeichnen müsse. Vielmehr sagten wir ihm lediglich mit reloadData, dass er neue Daten laden muss. Der View wusste daraufhin, dass neue Daten auch neue Zeichnerei bedeuten und markierte sich selbst zum Neuzeichnen. Nein, richtig ist das nicht: Ist etwa der Tableview bereits vollständig mit Daten bedeckt und fügen wir außerhalb des sichtbaren Bereiches einen neuen Eintrag an, so ist es freilich nicht notwendig, dass irgendwas neu gezeichnet wird. Es wäre ja ohnehin herausgescrollt und für den Nutzer nicht sichtbar. Der Scroller neben dem Tableview muss allerdings möglicherweise verkleinert werden, da die Liste sich verlängert hat. Allerdings dann nicht, wenn er bereits seine minimale Größe erreicht hat.
119
SmartBooks
Objective-C und Cocoa – Band 2
Aber auch an anderer Stelle gibt es dieses Problem: Änderten wir etwa den Namen einer Gruppe, die aus dem Tableview herausgescrollt war, in einem Textfeld, so musste das Textfeld neu gezeichnet werden, aber nicht der Eintrag im Tableview. Fenster
Änderung
Aaa noch ein Eintrag Hier geht's weiter Fenster
Änderung
Aaa noch ein Eintrag Hier geht's weiter
Ändert sich ein unsichtbarer Eintrag, dann ist kein Neuzeichnen erforderlich.
Nicht anders verhält es sich, wenn bei einem View einzelne Eigenschaften mittels eines Setters wie -setStringValue: (NSControl) gesetzt wird. Hier wird nur stets der gesamte View neu zu zeichnen sein. Aber das System bleibt: Jemand nimmt eine Änderung vor, und das View entscheidet daraufhin über seine Aktualisierung. Bedenken Sie: Nur das View kennt sein Layout und nur das View kann daher entscheiden, ob und wie neu gezeichnet werden muss. Ein View fängt dann aber nicht an, sich neu zu zeichnen. Nachdem es ermittelt hat, was neu gezeichnet werden muss, markiert es nur diese Bereiche. Das System sammelt derlei Aufrufe und schickt zu einem geeigneten Zeitpunkt eben ein besonderes Event. Dieses erfolgt also über die Event-Loop. (Band I, S. 398).
120
Kapitel 1
Graphische Ausgabe
Controller
① ändert
View ellipsenFarbe dreieckFarbe kreuzFarbe sternFarbe -zeichnen
② markiert ② markiert ⑤
④ fordert auf
Event-Loop
③ werden gesammelt
Das eigentliche Zeichnen ist von der Anforderung bis zur Durchführung im View verborgen.
Der Ablauf lässt sich also wie folgt zusammenfassen:
•
Ein Controller ändert einen Wert im View, sei es über Setter, sei es über die explizite Aufforderung, Daten neu zu holen (zum Beispiel reloadData).
•
Das View ermittelt die Bereiche, die aktualisiert werden müssen, und markiert diese im System.
• • •
Das System sammelt die Bereiche. Das System fordert zu gegebener Zeit das View auf, sich neu zu zeichnen. Das View kommt dieser Aufforderung nach und zeichnet sich erst jetzt.
Anforderung des Neuzeichnens Nach all dem muss also ein Controller in unserem View eine Änderung vornehmen und dieser muss sich daraufhin teilweise für ungültig erklären. Zu gegebener Zeit wird dann dieser Bereich neu gezeichnet. Wir wollen dies einmal exemplarisch implementieren: Wir bieten eine Methode an, Karten einem Stapel hinzuzufügen. Wenn dies geschieht, so muss natürlich der entsprechende Stapel neu gezeichnet werden. Fangen wir zunächst mit den Grundarbeiten an: Zunächst erstellen wir ein Outlet und eine Actionnmethode, mit der Karten hinzugefügt werden können:
121
SmartBooks
Objective-C und Cocoa – Band 2
@class TableBaizeView; @interface Card_GameAppDelegate : NSObject { IBOutlet TableBaizeView* tableBaizeView; … } @property (assign) IBOutlet NSWindow *window; - (IBAction)addCards:(id)sender; … @end
Die entsprechende Implementierung lautet: #import "TableBaizeView.h" @implementation Card_GameAppDelegate … - (IBAction)addCards:(id)sender { NSDictionary* card; NSMutableArray* cards = [NSMutableArray arrayWithCapacity:2]; card = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt:6], @"value", [NSNumber numberWithInt:0], @"suit", nil]; [cards addObject:card]; card = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt:12], @"value", [NSNumber numberWithInt:2], @"suit", nil]; [cards addObject:card]; [tableBaizeView addCards:cards toPlayerStackWithIndex:0]; }
Eigentlich ist das nichts Spannendes: Es werden halt zwei Karten als Dictionary erzeugt. Zu einem Array verpanscht fügen wir die dann bei dem TableBaizeView ein.
122
Kapitel 1
Graphische Ausgabe
AUFGEPASST Wieso so kompliziert mit einer speziellen Methode von TableBaizeView? Wir hatten ja die Subviews von unserem View zum Implementierungsdetail erklärt. Dann ist es richtig – und aus den Gründen der Kapselung vorteilhaft – den Zugriff alleine über den umfassenden View zuzulassen. Die aufgerufene Methode in TableBaizeView müssen wir bekannt machen und implementieren. TableBaizeView.h: @interface TableBaizeView { … } - (void) addCards:(NSArray*)extraCards toPlayerStackWithIndex:(NSUInteger)index; @end
TableBaizeView.m: @implementation TableBaizeView - (void) addCards:(NSArray*)extraCards toPlayerStackWithIndex:(NSUInteger)index { CardStackView* view = [self.playerViews objectAtIndex:index]; NSArray* cards = [view cards]; cards = [cards arrayByAddingObjectsFromArray:extraCards]; [view setCards:cards]; }
Es ist zugegebenermaßen eine sehr einfache Implementierung: Wir holen uns einfach das Array, bauen ein neues, indem wir die übergebenen Karten hinzufügen, und speichern das Ganze wieder ab. Im Nib verbinden Sie bitte den Menüpunkt File | New unseres Applikationsmenüs mit der Actionmethode. Also übersetzen, starten und testen. Sie werden vielleicht enttäuscht sein, denn ein Klick auf new bringt nichts. Wirklich nichts? Vergrößern Sie einmal das Fenster ein wenig. Sie kennen diesen Effekt schon aus dem ersten Band, allerdings in etwas abgewandelter Form: Die Daten werden zwar verändert, jedoch wird nichts gezeichnet. Wir müssen das eben jetzt explizit mitteilen. Fügen wir dies in CardStackView.m also noch ein:
123
SmartBooks
Objective-C und Cocoa – Band 2
@implementation CardStackView @synthesize alignment; // cards entfernt - (NSArray*)cards { return cards; } - (void)setCards:(NSArray*)value { // Aktualisierungsanforderung [self setNeedsDisplay:YES]; // Standardaccessor if( value != cards ) { [cards release]; cards = [value copy]; } }
Es wird also einfach nur ein Aufruf ergänzt, der das View markiert. Wenn Sie jetzt das Programm starten, wird eine entsprechende Aktualisierung auch vorgenommen werden. Bauen Sie jedoch im -drawRect: folgenden Testcode ein: - (void)drawRect:(NSRect)rect { NSLog( @"Bounds: %@", NSStringFromRect( [self bounds ] )); NSLog( @"Rect: %@", NSStringFromRect( dirtyRect )); … }
Das Rechteck, das neu angefordert wird, bedeckt jedes Mal unser gesamtes View: >… Bounds {{0, 0}, {75, 372}}: >… Rect {{0, 0}, {75, 372}}:
Dies ist ja auch logisch, da wir lediglich eine einfache Markierung vorgenommen hatten. Auch wenn wir konkret keine Performanceprobleme haben, so ist es generell eine gute Idee, sich genau zu überlegen, welche Bereiche wie neu gezeichnet werden müssen. Fügen wir Karten hinzu, so muss nur der Bereich der neuen Karten neu gezeichnet werden. Hier haben wir allerdings ein Problem: Da wir in dieser sehr einfachen Variante unserer Anbindung des Views an den Rest der Applikation einfach ein neues Array bekommen. Wir erhalten also keine Mitteilung darüber, was sich wirklich geändert hat.
124
Kapitel 1
Graphische Ausgabe
GRUNDLAGEN Bereits in Band I auf S. 428 hatte ich dazu ausgeführt, dass bei einer solchen umfänglichen Änderung das geänderte Objekt nicht mehr ohne weiteres weiß, was sich wirklich geändert hat. Wir werden daher auch im Kapitel über die View-Controller-Anbindung bessere Systeme (Data-Sources, KeyValue-Bindings) kennenlernen, die es erlauben, die spezifische Änderung einfach zu ermitteln. Dennoch, wir wollen hier nicht faul sein und dem Nutzer unseres Views unsere einfache Methode mit mehr Performance anbieten. Dazu vergleichen wir einfach das neue Array mit dem bisherigen und fischen die Änderungen heraus. Wir erzeugen uns zunächst eine private Kategorie von CardStackView, weil die Reihenfolge unserer Methoden nicht mehr trivial ist: @interface CardStackView( PrivateMethodsAddition ) - (NSRect)rectForCardWithIndex:(NSUInteger)index count:(NSUInteger)count getIsJammed:(BOOL*)jammed; @end @implementation CardStackView
Entsprechend passen wir die Markierung in unserem Setter an: - (void)setCards:(NSArray*)value { // Aenderungsrechtecke herausfinden NSRect dirtyRect; // Die Anzahl der vorhandene Karten und Stauchung holen NSUInteger oldCount = [cards count]; NSUInteger newCount = [value count]; BOOL oldJammed; BOOL newJammed; [self rectForCardWithIndex:0 count:oldCount getIsJammed:&oldJammed]; [self rectForCardWithIndex:0 count:newCount getIsJammed:&newJammed];
125
SmartBooks
Objective-C und Cocoa – Band 2
// Wenn mindestens ein Stapel gestaucht ist und sich die An// zahl der Karten veraendert, muss ohnehin das gesamte View // neu gezeichnet werden if( (oldJammed || newJammed) && (oldCount != newCount ) ) { [self setNeedsDisplay:YES]; // Andernfalls wissen wir hier, dass entweder keine Stauchung // vorliegt, der count also für die Berechnung der Rechtecke // unbeachtlich ist, oder aber beide Stapel dieselbe Anzahl // von Karten enthalten, so dass beide counts gleich sind. } else { // Anzahl der gemeinsamen Karten NSUInteger count = (oldCount < newCount)?oldCount:newCount; NSUInteger index; for( index = 0; index < count; index++ ) { id oldCard = [cards objectAtIndex:index]; id newCard = [value objectAtIndex:index]; // Unterschiedlicher Wert NSNumber* oldValue = [oldCard valueForKey:@"value"]; NSNumber* newValue = [newCard valueForKey:@"value"]; if( ![oldValue isEqualToNumber:newValue] ) { dirtyRect = [self rectForCardWithIndex:index count:count getIsJammed:&oldJammed]; [self setNeedsDisplayInRect:dirtyRect]; continue; ////////// } // Unterschiedliche Farbe NSNumber* oldSuit = [oldCard valueForKey:@"suit"]; NSNumber* newSuit = [newCard valueForKey:@"suit"]; if( ![oldSuit isEqualToNumber:newSuit] ) { dirtyRect = [self rectForCardWithIndex:index count:count getIsJammed:&oldJammed]; [self setNeedsDisplayInRect:dirtyRect]; continue; ////////// } }
126
Kapitel 1
Graphische Ausgabe
// Unterschiedliche Stapelhoehen beruecksichtigen: // index zeigt bisher auf erste "ueberstehende" Karte des // groesseren Stapels // Letzte Karte holen count = (oldCount > newCount)?oldCount:newCount; // Den Bereich zwischen den Karten markieren NSRect lastRect; dirtyRect = [self rectForCardWithIndex:index count:count getIsJammed:&oldJammed]; lastRect = [self rectForCardWithIndex:count-1 count:count getIsJammed:&oldJammed]; // Der gesamte Bereich muss neu gezeichnet werden. dirtyRect = NSUnionRect( dirtyRect, lastRect ); [self setNeedsDisplayInRect:dirtyRect]; } // Standardaccessor - darum ging es ja mal ... if( value != cards ) { [cards release]; cards = [value copy]; } }
Im Wesentlichen ist das pure Algorithmik, also nichts mit speziellem Bezug zu Objective-C und Cocoa. Wir haben daher die Erläuterungen gleich in den Sourcecode aufgenommen. Hier wäre es sicherlich auch denkbar, bei Karten, die nicht die oberste sind, nur den Versatz zu markieren. Denn diese Karten werden ja überwiegend durch die nächste Karte verdeckt. Aber wir sind nicht hier, um ein Kartenspiel auszuprogrammieren, sondern um die Funktionsweisen zu demonstrieren. Wir haben das daher gespart. Um den Effekt des Redraw-Rechteckes zu Lehrzwecken auch auf dem Schirm darzustellen, müssen wir jetzt etwas frickeln: Wir zeichnen das Rechteck abgedunkelt, welches -drawRect: übergeben wird. Nur haben wir dann das Problem, dass die letzte Darstellung nicht sicher entfernt wird, weil sie ja gerade nicht in der aktuellen Anforderung liegt. Sie würde daher stehen bleiben. Um diesen Effekt zu verhin127
SmartBooks
Objective-C und Cocoa – Band 2
dern, werden wir den Hintergrund testweise komplett weiß zeichnen. Allerdings wäre dies noch ohne Wirkung, da Cocoa die beschreibbare Fläche im View auf das aktuelle Rechteck beschränkt. Um dies zu verhindern, benötigen wir eine weitere Methode, welche das Clipping auf die markierten Rechtecke ausschaltet: - (BOOL)wantsDefaultClipping { return NO; } - (void)drawRect:(NSRect)rect { NSEraseRect( [self bounds] ); … // Aktuelles Rechteck markieren [[NSColor colorWithCalibratedWhite:0.0 alpha:0.2] setFill]; [NSBezierPath fillRect:dirtyRect]; }
Wenn Sie jetzt das Programm starten, werden Sie zunächst die kompletten Views überdeckt sehen. Dies liegt daran, dass ja zunächst die Views einmal ganz gezeichnet werden müssen. Fügen Sie jetzt aber Karten über das Menü hinzu, so wird nur der entsprechende Bereich neu gezeichnet. Das können wir in zweifacher Hinsicht noch verfeinern. Zum einen fügen Sie bitte eine weitere Methode in TableBaizeView.m hinzu: - (void)
addCards:(NSArray*)extraCards toPlayerStackWithIndex:(NSUInteger)index
{ … } - (void)changeCardWithIndex:(NSUInteger)cardIndex atPlayerStackWithIndex:(NSUInteger)index toCard:(id)card { CardStackView* view = [playerViews objectAtIndex:index]; NSMutableArray* cards = [NSMutableArray arrayWithArray:[view cards]]; [cards replaceObjectAtIndex:cardIndex withObject:card]; [view setCards:cards]; }
Diese machen Sie auch im Header bekannt: 128
Kapitel 1
Graphische Ausgabe
- (void)
addCards:(NSArray*)cards toPlayerStackWithIndex:(NSUInteger)index; - (void)changeCardWithIndex:(NSUInteger)cardIndex atPlayerStackWithIndex:(NSUInteger)index toCard:(id)card;
Entsprechend fügen wir im App-Delegate eine Action-Methode hinzu: - (IBAction)changeAndAddCards:(id)sender { NSDictionary* card; card = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt:2], @"value", [NSNumber numberWithInt:2], @"suit", nil]; [tableBaizeView changeCardWithIndex:1 atPlayerStackWithIndex:0 toCard:card]; [self addCards:sender]; } @end
die Sie ebenfalls im Header bekannt machen - (IBAction)changeAndAddCards:(id)sender; @end
Im Interface Builder fügen Sie einfach unter New einen neuen Eintrag ins Menü ein und verbinden diesen mit der neuen Actionmethode im App-Delegate. Um den Effekt auch zu sehen, ändern wir kurzfristig sowohl die Methode zur Berechnung des Kartenrechteckes als auch -drawRect: - (NSRect)rectForCardWithIndex:(NSUInteger)index count:(NSUInteger)count getIsJammed:(BOOL*)jammed { … // Verschiebe Karte um Index rect = NSOffsetRect( rect, xStep * index, yStep * index ); if( self.alignment == NSMaxYEdge ) { rect.origin.x += 4.0 * index;
129
SmartBooks
Objective-C und Cocoa – Band 2
} … } … - (void)drawRect:(NSRect)rect { … // Aktuelles Rechteck markieren … // Rechteckteile markieren const NSRect* rects; NSInteger rectCount; [self getRectsBeingDrawn:&rects count:&rectCount]; for( index = 0; index < rectCount; index++ ) { [NSBezierPath fillRect:rects[index]]; } }
Die erste Änderung sorgt lediglich dafür, dass die Karten seitlich versetzt gezeichnet werden. Dies macht den Effekt, den wir hier verdeutlichen wollen, stärker. In -drawRect: geschieht etwas Erklärungsbedürftiges: Wir holen uns mit der Methode -getRectsBeingDrawn:count: eine Liste von Rechtecken ab, deren Anzahl wir im zweiten Parameter count erfahren. Um es kurz zu machen – und auf die ObjectiveC-Referenz zu verweisen: Es handelt sich um ein sogenanntes C-Arrays, bei dem zu einem Typ (NSRect) mehrere Variablen hintereinander angelegt werden. Um an die einzelnen Elemente zu gelangen, muss der Index ab 0 zählend in eckigen Klammern geschrieben werden. Genau dies geschieht in der Schleife. Hier geht es aber um etwas anderes: Diese Liste enthält nicht das umfassende Rechteck der Zeichenanforderung, sondern jede einzelne Anforderung. Da wir das Rechteck zu jeder einzelnen Anforderung noch einmal abdunkeln, werden diese sichtbar. Starten Sie einfach einmal das Programm und fügen Sie ein paar Karten hinzu. Dann rufen Sie über die neue Methode -changeAndAddCards: auf. Sie werden sowohl die einzelnen Rechtecke für die Änderung wie das Hinzufügen sehen und dann ebenfalls das umfassende Rechteck. Bitte machen Sie die Änderung in rectForCardWith Index:count:getIsJammed: wieder rückgängig, damit die Karten wieder untereinander erscheinen. Gut, das war aber nur zur Verdeutlichung, was passiert. Üblicherweise wird man sich gar nicht mit ekligen C-Arrays herumschlagen. Für einen schnellen Vergleich einzelner Bereiche in unserem View mit dem übergebenen rect-Parameter existiert die wesentlich handlichere Methode -needsToDrawRect: (NSView), welche für ein 130
Kapitel 1
Graphische Ausgabe
bestehendes Rechteck wie unsere Kartenrechtecke ermittelt, ob es in dem zu aktualisierenden Bereich liegt. Diese benutzen wir wieder und löschen den Teil zur Anzeige der Markierungen: - (void)drawRect:(NSRect)rect { … for( index = 0; index < count; index++ ) { id card = [self.cards objectAtIndex:index]; cardRect = [self rectForCardWithIndex:index count:count getIsJammed:&jammed]; if( [self needsToDrawRect:cardRect] ) { [self drawCardBorderInRect:drawRect]; suit = [[card valueForKey:@"suit"] integerValue]; value = [[card valueForKey:@"value"] integerValue]; [self drawCardFaceWithSuit:suit value:value inRect:cardRect]; } } }
Löschen Sie auch wieder die Methode -wantsDefaultClipping. Denn sie führt so zu einem Anzeigefehler, wenn wir nur obere Karten neu zeichnen und dabei das Clipping nicht begrenzen. Es kann dann nämlich eine tiefere Karte übermalt werden, ohne dass diese ebenfalls neu gezeichnet wird, weil sie nicht im Clippingbereich liegt. Sie sehen schon: Derlei Optimierungen sind nicht ohne Risiko.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 19« von der Webseite herunterladen.
131
SmartBooks
Objective-C und Cocoa – Band 2
Drucken Ein weiterer Punkt ist freilich das Drucken. Im Grunde genommen haben wir bereits dafür alles getan. Versuchen Sie es einfach mal … (aber besser als PDF-Ausdruck und dann in der Vorschau anschauen). Allerdings werden Sie enttäuscht sein: Zum einen erscheint das gesamte Fenster nebst den Fensterelementen in der Titelzeile, zum anderen sind unsere Haarlinien verschwunden. Das werden wir jetzt verbessern. An einem Druckvorgang sind mehrere beteiligt:
•
ein View, welches die Daten zeichnet (auch dann, wenn sie auf den Drucker gehen).
•
ein Page-Layout, welches unser Seitenlayout beschreibt (also nicht das Format der Seiten im Drucker).
• •
eine Print-Operation, welche den Duckablauf steuert. eine Print-Info, welches Informationen für die Print-Operation hält.
Page-Layout Gehen wir die Sachen an und implementieren wir zunächst das Page-Layout. Unser Spiel hat bisher keine Seitengröße. Wir spielen ja nicht auf einem Blatt Papier. Da aber der Code schöne Beispiele für das Drucken enthält, erzeugen wir kein neues Projekt, sondern bauen unser Spiel entsprechend um. Da unser Spiel ein Seitenformat bekommt, ist nicht mehr sichergestellt, dass ein entsprechendes Fenster auf den Monitor passt. Wir legen daher das Tablebaizeview in einen Scrollview und referenzieren dies aus dem App-Delegate. Die Actionmethoden und das bisherige Outlet löschen Sie bitte wieder. Da wir einzelne Kartentische dynamisch erzeugen werden, ergibt dieses Outlet keinen Sinn mehr. Die entsprechenden Methoden in Tablebaizeview können Sie ebenfalls entfernen, nicht aber unseren aufwendig programmierten Setter. Im Application-Delegate fügen Sie bitte dafür ein neues Outlet ein: @interface Card_GameAppDelegate : NSObject { … IBOutlet NSScrollView* scrollView; } @end
132
Kapitel 1
Graphische Ausgabe
Entsprechend müssen wir also im Interface Builder mit Hilfe der Funktion Layout | Embed Objects in | Scroll View unser Tablebaizeview in einen Scrollview stopfen. Bitte erledigen Sie das. Das Autoresizing des Scrollviews setzen Sie so, dass es sich mit dem Fenster vergrößert (also alle Linien angewählt), das des TablbBaizeviews so, dass es das gerade nicht macht, sondern zentriert ist (alle Pfeile aus). Der Spieltisch soll sich ja nicht mit dem Fenster und dem Scrollview vergrößern. Setzen Sie jetzt noch das Outlet vom App-Delegate zum Scrollview. Page-Layout abfragen Im AppDelegate holen wir uns die voreingestellte Größe ab und setzen entsprechend die Größe des Tablebaizeviews: - (void)awakeFromNib { NSPrintInfo* info = [NSPrintInfo sharedPrintInfo]; NSSize paperSize = [info paperSize]; paperSize.width -= [info leftMargin] + [info rightMargin]; paperSize.height -= [info topMargin] + [info bottomMargin]; NSView* documentView = [scrollView documentView]; [documentView setFrameSize:paperSize]; }
Die Print-Info ist eine globale Instanz (Singleton), die die aktuellen Eigenschaften für das Drucken enthält. Hier erfahren wir also die Größe unseres virtuellen Blattes. (Und immer noch nicht des Papiers im Drucker. Dieses interessiert uns ja noch gar nicht, da wir noch nicht einmal einen Drucker gewählt haben.) Das hier verwendete Documentview ist jenes, welches das Scrollview darstellt (Band I, S. 403 ff.). Wenn Sie das Programm starten, werden Sie jedoch vermutlich wenig begeistert sein. Der Grund ist, dass aller Wahrscheinlichkeit nach ein Format vorbesetzt ist, welches eine senkrechte Ausrichtung der Seite vorsieht. Das ist für das Layout unseres Spieles natürlich wenig dienlich. Page-Layout setzen Als Erstes setzen wir daher die Ausrichtung: - (void)awakeFromNib { NSPrintInfo* printInfo = [NSPrintInfo sharedPrintInfo]; … }
[info setOrientation:NSLandscapeOrientation];
133
SmartBooks
Objective-C und Cocoa – Band 2
Das sieht nach dem Programmstart schon gleich besser aus. NSLandscapeOrientation (Landschaftsausrichtung) bedeutet eine waagerechte Lage des Papiers. Die (bisherige) senkrechte Ausrichtung hat den Wert NSPortraitOrientation (Portraitausrichtung). User-Interface implementieren Allerdings sollte der Benutzer das Seitenformat setzen können. So ist er dies schließlich von Mac-Anwendungen gewöhnt. Dazu implementieren wir zwei Methoden im App-Delegate: @synthesize window; - (void)pageLayoutDidEnd:(NSPageLayout*)layout return:(NSInteger)code context:(void*)context { NSPrintInfo* info = [layout printInfo]; [NSPrintInfo setSharedPrintInfo:info]; NSView* documentView = [scrollView documentView]; NSSize paperSize = [info paperSize]; paperSize.width -= [info leftMargin] + [info rightMargin]; paperSize.height -= [info topMargin] + [info bottomMargin]; [documentView setFrameSize: paperSize]; } - (IBAction)runPageLayout:(id)sender { // Ein Panel erzeugen NSPageLayout *pageLayout = [NSPageLayout pageLayout]; // und starten NSWindow* window = [scrollView window]; NSPrintInfo* printInfo = [NSPrintInfo sharedPrintInfo]; SEL selector = @selector( pageLayoutDidEnd:return:context: ); [pageLayout beginSheetWithPrintInfo:printInfo modalForWindow:window delegate:self didEndSelector: selector contextInfo:NULL]; }
Wir erzeugen also ein Page-Layout-Panel und starten dieses. Falls es beendet ist, wird unsere Methode -pageLayoutDidEnd:return:context: ausgeführt, die dann den neuen Wert dem globalen Print-Info-Singleton entnimmt und unser View entsprechend setzt. 134
Kapitel 1
Graphische Ausgabe
Wir haben jetzt also in unserer Anwendung ein Blatt Papier modelliert, auf dem sich das Spiel befindet. Der Nutzer kann das Format setzen. In MainMenu.xib verbinden Sie bitte den Menüpunkt File | Page Setup des Applikationsmenüs mit der neuen Actionmethode. Speichern, starten und über das Menü testen.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 20« von der Webseite herunterladen.
Print-Operation Dass man bereits von Anfang an drucken konnte, findet seine Ursache darin, dass das System eine Actionmethode -print: aufruft. Diese wird wiederum durch die geheimnisumwobene Responder-Chain geleitet. Sie können das nachprüfen, indem Sie im Interface Builder den entsprechenden Menüeintrag anwählen und sich die Aktion anzeigen lassen. Print-Action anbieten Ohne dem Kapitel über Events und Responder vorzugreifen, findet man eine entsprechende Methode in der Fensterklasse NSWindow. Daher ist das Fenster der Meinung, sich drucken zu müssen. Da diese Funktionalität bereits in Cocoa enthalten ist, geschieht eben auch genau das. Damit wir selbst drucken können, müssen wir vorher die Nachricht aus der Responder-Chain fischen. Dies geht einfach in der Weise, dass wir sie in unserem View implementieren: @implementation TableBaizeView - (BOOL)acceptsFirstResponder { return YES; } - (IBAction)print:(id)sender { NSLog( @"private View" ); }
Wenn Sie jetzt auf Drucken klicken – passiert gar nichts außer der Mitteilung im Log. Darum kümmern wir uns sogleich. Hier geht es noch um das seltsame -acceptsFirstResponder. Diese Methode gibt an, ob wir überhaupt Responder sein 135
SmartBooks
Objective-C und Cocoa – Band 2
können, also Action-Nachrichten über die Responder-Chain empfangen wollen. Da wir auf oberster Ebene der einzige View in dem Fenster sind, werden wir automatisch First-Responder. Print-Action implementieren Vermutlich hat es Sie gewundert, dass jetzt nicht einmal mehr der Druckdialog erscheint. Immerhin haben wir ja ein -print: implementiert, welches auch benutzt wurde. Der Witz an der Sache ist, dass diese Methode unmittelbar nach dem Klick im Menü ausgeführt wird. Sie ist dann dafür verantwortlich, die Drucksitzung zu beginnen, also auch den Druckdialog zu öffnen. Keine Panik! Dies bedeutet jetzt nicht, dass Sie das alles von Hand programmieren müssen. Vielmehr muss eine Instanz der Klasse NSPrintOperation erzeugt werden, die dann auf das System losgelassen wird. Bauen wir das in unsere Methode ein: - (IBAction)print:(id)sender { NSPrintOperation* operation = [NSPrintOperation printOperationWithView:self]; [operation runOperation]; }
TIPP Es kann dienlich sein, nur für das Drucken ein eigenes View zu erzeugen, welches zu Seiteneinstellungen passt. Das ist prinzipiell möglich, wenn sich Ihr View ohne Weiteres instantieren lässt. Ein weiterer Aspekt hiervon ist, dass der Ausdruck derart anders vorgenommen wird, dass sich nicht eine einheitliche Druckmethode sinnvoll formulieren lässt. Denken Sie etwa an eine datenbankähnliche Applikation mit einem Tableview, bei der ein Report als Liste gedruckt werden soll. Das macht man dann doch besser selbst über einen eigenen View. Das sieht doch schon ganz schön aus! Dass immer noch die Kartenränder fehlen, verstehen Sie jetzt vielleicht schon: Wir hatten diese ja mit einer Haarlinie gedruckt und ausgerichtet, weil wir das Antialiasing auf dem Bildschirm »umgehen« wollten. Und das ist hier jetzt wenig sinnvoll. Bedenken Sie, dass wir zum Drucken den ganz normalen -drawRect: benutzen. Zeichnen auf das Ausgabegerät anpassen Wir müssen also die Art des Zeichnens davon abhängig machen, wohin wir zeichnen. Dies ist glücklicherweise einfach mit der Methode -isDrawingToScreen (NSGraphicsContext). Ändern wir entsprechend den CardStackView: 136
Kapitel 1
Graphische Ausgabe
- (void)drawRect:(NSRect)rect { … for( index = 0; index < count; index++ ) { id card = [self.cards objectAtIndex:index]; NSGraphicsContext* context = [NSGraphicsContext currentContext]; NSRect cardRect; if( [context isDrawingToScreen] ) { cartRect = [self rectForCardWithIndex:index count:count getIsJammed:&jammed]; } else { cartRect = [self printRectForCardWithIndex:index count:count getIsJammed:&jammed]; } … } }
Neben der Anpassung der Rechteckberechnung müssen zudem die Linienbreiten geändert werden: - (void)drawCardBorderInRect:(NSRect)rect { … NSGraphicsContext* context = [NSGraphicsContext currentContext]; if( [context isDrawingToScreen] ) { [path setLineWidth:0.0]; } else { [path setLineWidth:1.0]; } … }
Nun sollte auch der Druck wie erwartet aussehen.
137
SmartBooks
Objective-C und Cocoa – Band 2
Paginierung Paginierung ist die Aufteilung in Seiten. Grundsätzlich hat dieses Problem zwei Punkte zum Abarbeiten: Die Herstellung von Seiten in unserer Applikation und die Ausgabe Seite für Seite: Seiten implementieren Und auch hier unternehmen wir das nach dem Prinzip »What you see is what you get« (WYSIWYG): Wir legen einzelne Seiten für verschiedene Spiele an. Um das einfach zu gestalten, packen wir diese in einen umfassenden View, der die einzelnen Seiten als Subview verwaltet. Hierzu erzeugen wir uns zunächst eine neue Klasse ContainerView als Subklasse von NSView (Vorlage: Objective-C class | NSView subclass). Diese passen wir uns in ContentView.m an: Zum einen zeichnen wir einen Rand um unser View, damit Sie es später besser auf dem Bildschirm erkennen können. Zum anderen müssen wir die Methode -addSubview verändern. Wir wollen selbst bestimmen, wann wie und wo der neue Kartentisch erscheint und daraufhin unseren View in seinen Dimensionen verändern. static const CGFloat border = 8.0; @implementation ContainerView … - (void)drawRect:(NSRect)rect { [[NSColor darkGrayColor] setFill]; NSFrameRect( [self bounds] ); //Schatten um die Subviews, damit es schoener aussieht for( NSView* subview in [self subviews] ) { NSShadow* shadow = [[[NSShadow alloc] init] autorelease]; [shadow setShadowBlurRadius:border]; [shadow set]; NSRectFill( [subview frame] ); } } - (void)addSubview:(NSView*)subview { NSRect subviewFrame = subview.frame; // Die Lage des neuen Views ergibt sich aus dem Container: subviewFrame.origin.x = border; subviewFrame.origin.y = NSHeight( self.bounds ); [subview setFrameOrigin:subviewFrame.origin];
138
Kapitel 1
Graphische Ausgabe
// Die Groesse des Containers anpassen NSSize containerSize = self.frame.size; containerSize.height += subviewFrame.size.height + border; CGFloat minWidth = subviewFrame.size.width + border + border; if( containerSize.width < minWidth ) { containerSize.width = minWidth; } [self setFrameSize:containerSize]; // Das neue View dem Container hinzufuegen [super addSubview:subview]; }
Im Prinzip handelt es sich hierbei um ein paar Berechnungen für die Ausmaße des Views. Bedenken Sie aber noch eines, auch wenn hier Bounds und Frame größengleich sind: Die Koordinaten des Subviews errechnen sich aus den Bounds des Containerviews, da der Subview ja im Containerview liegt. Die neue Größe des Containerviews bezieht sich aber auf das Frame, da es jetzt ja um die Lage des Containerviews in seinem Superview geht. Natürlich müssten wir umgekehrt noch die Berechnung vornehmen, wenn wir wieder einen Subview entfernen. Wir ersparen uns das hier nur, weil es nicht das Thema ist. Sie können das zur Übung freilich hinzufügen. Schließlich müssen wir die Methoden, die bisher im Tablebaizeview für den Ausdruck sorgten, dort löschen und zum Containerview transferieren: @implementation ContainerView - (BOOL)acceptsFirstResponder { return YES; } - (IBAction)print:(id)sender { NSPrintOperation* operation = [NSPrintOperation printOperationWithView:self]; [operation runOperation]; }
Mehr Arbeit ist im App-Delegate zu erledigen: Zum einen muss jetzt das Hinzufügen organisiert werden. Hierzu deklarieren wir eine Actionmethode im App-Delegate:
139
SmartBooks
Objective-C und Cocoa – Band 2
@interface Card_GameAppDelegate : NSObject { … } … - (IBAction)addTable:(id)sender; @end
Die Implementierung der neuen Methode: - (IBAction)addTable:(id)sender { NSView* documentView = [scrollView documentView]; // Bemassung holen NSRect frame = NSZeroRect; NSPrintInfo* info = [NSPrintInfo sharedPrintInfo]; frame.size = [info paperSize]; frame.size.width -= [info leftMargin] + [info rightMargin]; frame.size.height -= [info topMargin] + [info bottomMargin]; // einen View entsprechender Groesse erzeugen und platzieren TableBaizeView* view = [[[TableBaizeView alloc] initWithFrame:frame] autorelease]; // Die Groesse des Containers anpassen [documentView addSubview:view]; } - (void)awakeFromNib {
Die Print-Info enthält als globaler Singleton stets die letzte Einstellung des Nutzers. Bei einer Änderung der Einstellung wären daher sämtliche Subviews und der Containerview in -pageLayoutDidEnd:return:context: neu zu berechnen. Wir haben hier davon abgesehen, da der Umgang mit Subviews mittlerweile klar sein sollte. Sie können dies als Übungsaufgabe programmieren.
140
Kapitel 1
Graphische Ausgabe
TIPP Im Sinne des MVC-Musters wäre es richtig schön, wenn Sie im Containerview eine Methode anböten, mit der sich ein Subview in der Größe (aber nur der Größe) verändern lässt, worauf sich das Containerview neu berechnet. In unserer Struktur ist es ja so, dass vom Containerview aus betrachtet die Größe eine äußere Eigenschaft ist, die Platzierung der Subviews indessen eine gekapselte interne Eigenschaft. Da wir aber dann das Layout nur noch bei der Einfügung eines neuen Kartentisches benötigen, besteht kein Bedarf mehr, sich selbst in das Page-Layout zu hängen. Wir können daher AppDelegate.m vereinfachen: @implementation Card_GameAppDelegate - (IBAction)runPageLayout:(id)sender { [[NSApplication sharedApplication] runPageLayout:sender]; }
Schließlich ist das -awakeFromNib wieder zu reduzieren: - (void)awakeFromNib { NSPrintInfo* info = [NSPrintInfo sharedPrintInfo]; [info setOrientation:NSLandscapeOrientation]; [[scrollView documentView] setFrame:NSZeroRect]; }
Die letzten Arbeiten sind im Interface Builder zu erledigen: Zum einen muss das View im Scrollview jetzt die Klasse Containerview anstelle von Tablebaizeview erhalten. Geben Sie das im Inspector ein. Schließlich benennen wir den Menüpunkt New im File-Menü in New Table um und ziehen die Action auf die neue Methode -addTable: im App-Delegate. Testen Sie bitte, ob sich neue Tische der Ansicht hinzufügen lassen.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 21« von der Webseite herunterladen. Seiten ausgeben Haben wir erst einmal Seiten, so lassen sich diese leicht ausgeben. Während des Druckvorganges, also letztlich durch die Instanz von NSPrintOperation, werden verschiedene Informationen abgefragt, um jeweils Teile des geruckten Views – bei 141
SmartBooks
Objective-C und Cocoa – Band 2
uns der Containerview – auf einzelne Seiten zu verteilen. Da wir für jede Seite einen eigenen View angelegt hatten, ist dies vergleichsweise einfach. Zunächst müssen wir angeben, wie viele Seiten gedruckt werden sollen. Dies entspricht bei uns der Anzahl der Subviews. Nach -drawRect: fügen Sie bitte im Container-View folgende Methode ein: - (BOOL)knowsPageRange:(NSRangePointer)range { (*range).location = 1; (*range).length = [[self subviews] count]; return YES; } …
GRUNDLAGEN Die Klammerung ist notwendig, damit zunächst der Zeiger auf die RangeStruktur aufgelöst wird. Es gibt eine einfachere Syntax in C, die in der Referenz wiederum zusammengefasst erläutert wird. Nachdem nun mitgeteilt wurde, wie viele Seiten erzeugt werden sollen, wird der View für jede Seite befragt, welcher Bereich auf ihr landen soll. Hier geben wir einfach den Frame jeden Subviews aus, so dass genau dieses auf der Seite landet. Fügen Sie die Methode nach der vorangegangenen ein: … - (NSRect)rectForPage:(NSInteger)page { return [[[self subviews] objectAtIndex:page-1] frame]; }
Bitte beachten Sie, dass der Index um eines vermindert werden muss, da ja die Subviews in einem ab 0 zählenden Array liegen, die Seitenzählung indessen mit 1 beginnt. Sie können jetzt mehrere Seiten anlegen und drucken. Schon in dem Druckdialog werden automatisch die einzelnen Seiten angezeigt.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 22« von der Webseite herunterladen.
142
Kapitel 1
Graphische Ausgabe
Core Graphics Die hier vorgestellte Zeichenfunktionalität ist gleichermaßen »eine Ebene tiefer« durch ein C-Framework namens Core Graphics abgebildet. Sie werden bemerken, dass dieses die hier angesprochenen Technologien und Konzepte nahezu 1 zu 1 umsetzt, sogar die Bezeichnungen sind ähnlich: Aus NSAffineTransform wird CG AffineTransform, NSBezierPath zu CGMutablePathRef usw. Konzeptionell werden Sie das also verstehen, wenn Sie die Cocoa-Schicht verstanden haben. Aufgrund dieser Ähnlichkeit ist es für die Programmierung auf dem Mac in der Regel nicht notwendig, sich damit auseinanderzusetzen. Es gibt allerdings Gründe, warum dies dennoch erforderlich wird:
•
Core Graphics bietet ausnahmsweise eine Möglichkeit, die die Cocoa-Pendants nicht beinhalten. Bei OS X 10.4 war es etwa so, dass Gradients nur als CoreGraphics-Gradients verfügbar waren, nicht aber als Cocoa-Klasse NSGradient.
•
Manche Technologien wie Core Animation und Core Image setzen auf Core Graphics auf. Es ist dann erforderlich, zwischen den Welten zu wechseln.
•
Auf dem iPhone wird die Zeichnerei grundsätzlich über Core Graphics abgewickelt.
Aus diesem Grunde soll an dieser Stelle der gemischte Umgang verdeutlicht werden. Die Systematik hierbei ist verallgemeinerungsfähig. Sie erhalten hier also auch Wissen, wenn Sie aus einem Objective-C-Programm heraus auf andere Technologien zugreifen wollen, die in C implementiert sind. Die Core Foundation ist hier der Klassiker. Sie enthält etwa eine Sammlung von Basis-»Klassen« wie Container als C-Implementierung.
Core-Graphics-»Objekte« Auch wenn Core Graphics ein C-Framework ist, von Haus aus also keine Objekte kennt, so ist die tatsächliche Implementierung an die objektorientierte Programmierung angelehnt.
Objektorientierte Behandlung von Strukturen Vieles von dem, was in Cocoa durch Instanzen einer Klasse dargestellt wird, ist in Core Graphics eine statische C-Struktur. Als Beispiel nehmen wir das soeben erwähnte CGAffineTransform. Es handelt sich also nicht mehr um Objekte im technischen Sinne. Um sich unter anderem eine leere Transformation geben zu lassen und sodann zu verändern, dient folgender Code: 143
SmartBooks
Objective-C und Cocoa – Band 2
// Variable anlegen CGAffineTranform transform; // Drehung um 45° transform = CGAffineTransformMakeRotation( 45 ); // Eine Skalierung hinzufuegen transform = CGAffineTransformScale( transform, 1.5, 1.8 );
Dabei ist CGAffineTransform eine simple C-Struktur mit der Definition: struct CGAffineTransform { CGFloat a; CGFloat b; CGFloat c; CGFloat d; CGFloat tx; CGFloat ty; };
Falls Sie erfahrener C-Programierer sein sollten, bemerken Sie schon, dass das System aber objektorientiert arbeitet: Die entsprechenden Funktionen bekommen eine vorhandene C-Struktur als »Objekt« und arbeiten darauf. Dies ist wichtig: Auch wenn Sie es mit einer Struktur zu tun haben, benutzen Sie bitte stets nur die Funktionen des Frameworks, um mit dieser zu arbeiten. Sie greifen nicht unmittelbar auf die Komponenten der Struktur zu. Der Unterschied zeigt sich dadurch, dass eine Struktur in Objective-C wie ein Skalar By-Value behandelt wird, also etwa kopiert werden kann oder als Rückgabewert eine Methode dient: // Einfache Zuweisung wie bei einem Skalar CGAffineTransform transform2 = transform1; // nicht: CGAffineTransfrom transform2 = CGAffineTransformCopy( transform1 );
Und natürlich werden keine Nachrichten an eine solche Struktur gesendet, sondern eben Funktionen auf die Struktur angewendet. Speicherverwaltungsregeln müssen Sie nicht beachten, da die Struktur selbst in einer Variablen gespeichert wird und mit ihr verschwindet.
C-»Objekte« Daneben existieren Strukturen, die zu komplex sind, um sie als einfache Variable zu behandeln. Sie erkennen sie an dem Suffix »Ref«, welches eben signalisiert, dass 144
Kapitel 1
Graphische Ausgabe
wie bei Objective-C nur eine Referenz in der Variablen gespeichert wird anstelle des Objektes selbst. Der Grund dafür ist, dass ihr Inhalt dynamisch ist, es also zu Problemen der Speicherverwaltung kommt. Ein Beispiel sei hier CGMutablePathRef, was einer Instanz von NSBezierPath entspricht. Da verschiedene Segmente zu einem solchen Pfad hinzugefügt werden können, reicht eine einfache Struktur zur Beschreibung nicht aus. Vielmehr wird außerhalb der Struktur bei Bedarf Speicherplatz vom System angefordert. Das führt dazu, dass dieser Speicherplatz wieder freigegeben werden muss, wenn die Ausgangsstruktur zerstört wird. - (void)method { // Eine Variable anlegen: C-Objekt, also nur ein Zeiger (Ref) CGMutablePathRef* pathRef ; // Objekt erzeugen: "Allocator" pathRef = CGPathCreateMutable(); // Objekt bearbeiten: Speicher wird angefordert CGPathMoveToPoint( pathRef, NULL, 3.0, 4.0 ); CGPathLineToPoint( pathRef, NULL, 7.4, 5.6 ); // Speicherloch: // Das Path-Objekt wird nicht freigegeben, nur die Path-Ref }
Wenn diese Methode verlassen wird, wird automatisch die Variable pathRef gelöscht. Es handelt sich ja um einen üblichen C-Zeiger. Einen solchen haben wir noch nie explizit aus dem Speicher entfernt. Hinter dem Zeiger befindet sich aber eine dynamische Struktur. Da wir es jedoch nicht mit einem Objekt im technischen Sinne zu tun haben, wird nicht automatisch ein gedachtes dealloc() aufgerufen. Aus diesem Grunde werden diese Objekte wie Objective-C-Objekte behandelt: Sie werden mittels einer Allocator-Funktion erzeugt. Damit aber der dahinter stehende Speicher ebenfalls freigegeben wird, muss ein explizites release() erfolgen. Obiger Code sieht also richtig so aus: - (void)method { // Eine Variable anlegen: C-Objekt, also nur ein Zeiger (Ref) CGMutablePathRef pathRef ;
145
SmartBooks
Objective-C und Cocoa – Band 2
// Objekt erzeugen: "Allocator" pathRef = CGPathCreateMutable(); … // Freigabe CGPathRelease{ pathRef ); }
Da jedoch auf der C-Ebene kein Autorelease-Pool existiert, muss die Freigabe, wie hier gezeigt, stets explizit mit einer Release-Funktion erfolgen. Convenience-Allo cators existieren aus gleichem Grunde ebenfalls nicht. Allerdings ist auf C-Ebene Reference-Counting implementiert, so dass man mit Retain-Funktionen arbeiten kann. Dies führt in der Praxis dazu, dass bei der Rückgabe eines C-Objektes der Aufrufer für die Freigabe verantwortlich ist. Ein paar Regeln:
•
C-Objekte werden wie Objective-C-Objekte über einen Zeiger behandelt. Der Datentyp trägt Ref im Namen.
• • •
C-Objekte werden mit Funktionen erzeugt, die Create im Namen tragen.
•
Das Ergebnis ist ein C-Objekt, welches einen Reference-Count von 1 hat. Da kein Autorelease-Pool existiert, müssen diese mit Create-Funktionen erzeugten C-Objekte stets mit einer Release-Funktion explizit wieder freigegeben werden. Es existiert jedoch Reference-Counting, so dass man Setter mit Retain- und Release-Funktionen bauen kann, wie man es auch in Objectve-C machen würde.
Wie bereits gesagt, dies ist verallgemeinerungsfähig und gilt nicht nur für Core Graphics. Wenn Sie es daher mit einem C-Objekt zu tun haben, für das keine explizite Release-Funktion existiert, so nehmen Sie einfach die allgemeine Funktion aus der Core Foundation CFRelease(). Auf die Core Foundation kommen wir gleich noch zu sprechen.
Umwandlung Wenn wir es mal mit C und mal mit Objective-C zu tun haben, so stellt sich die Frage, wie ein Austausch zwischen den beiden Schichten vorgenommen werden kann. Je nach Anwendungsfall existieren verschiedene Ansätze:
Umwandlungsfunktionen Zuweilen werden die verschiedenen Typen mittels expliziter Funktion gewandelt. Dies gilt sogar für einfache Strukturen:
146
Kapitel 1
Graphische Ausgabe
// Ein Core-Graphics-Rechteck CGRect coreRect = CGRectMake( 0.0, 0.0, 4.0, 5.0 ); // Umwandeln in ein Cocoa-Rechteck NSRect cocoaRect = NSRectFromCGRect( coreRect );
Wenn Sie eine solche Umwandlungsfunktion vorfinden, ist diese freilich zu benutzen. Diese Umwandlungsfunktionen existieren übrigens nicht nur für (statische) C-Strukturen wie CGRect., sondern auch für C-Objekte. Dabei kann es aber sein, dass der Aufbau etwas anders ist, so dass die Umwandlung nicht 1 zu 1 vonstatten geht. So lassen sich etwa CGImages unmittelbar nur aus der Bitmap-Repräsentation eines NSImage erzeugen, nicht aus dem Image selbst: // Image als Cocoa-Instanz NSImage* cocoaImage = [NSImage imageNamed:…]; // In eine Core-Graphics-Instanz umwandeln // Jedoch stellt ein CGImage immer eine Bitmap-Repraesentation // dar. Wir nehmen an, dies sei die erste Repräsentation des // Images: NSBitmapImageRep* cocoaBitmap = [[cocoaImage representations] objectAtIndex:0] // Jetzt die Umwandlung CGImageRef coreImage = [cocoaBitmap CGImage]; … // Ein C-Objekt muss wieder freigegeben werden: CGImageRelease( coreImage );
Wir werden diese Art der Umwandlung im Verlaufe des Buches noch benötigen.
Manuelles Umwandeln Manchmal kann ein Objective-C-Objekt nicht unmittelbar in ein C-Objekt umgewandelt werden. In diesen Fällen kann man häufig über einen von beiden Objekten unterstützten (meist allgemeinen) Datentypen wie NSData bzw. CFData gehen. Auch hierzu folgen noch Beispiele im Verlauf des Buches.
Toll-Free-Bridging Eine weitere Möglichkeit zur – man kann es dann eigentlich nicht so nennen – Umwandlung ist das Toll-Free-Bridging. (In Amerika muss man häufig für die Benutzung einer Brücke Gebühren zahlen.) Die Technik dahinter ist, dass C-Objekte erzeugt werden, die so aufgebaut sind, dass sie von dem Laufzeitsystem von Objective-C nicht von Objective-C-Objekten unterschieden werden können. Das führt dazu, dass sie im Code austauschbar sind. 147
SmartBooks
Objective-C und Cocoa – Band 2
Verdeutlicht werden soll das an einem anderen C-Framework, welches auf niedrigerer Ebene etwa die Containerklassen wie NSString, NSArray usw. nachbildet: Core Foundation. Hat man hier ein C-Objekt erzeugt, so lässt sich dies einfach als Objective-C-Objekt verwenden: // Ein Core-Foundation-Objekt CFStringRef coreString = CFStringCreateWithCString( "Hallo" ); // CF-Strings sind Toll-Free-Bridges und können daher wie // Objective-C-Objekte verwendet werden: NSInteger letterCount = [(NSString*)coreString length]; // Sie muessen aber als C-Objekte freigegeben werden CFRelease( coreString );
GRUNDLAGEN Auch wenn häufig tatsächlich die Cocoa-Klassen C-Objekte zurückliefern, es also in Wahrheit gar keine Cocoa-Klasse gibt, so darf dies nicht darüber hinwegtäuschen, dass Apple nicht funktionale Kompatibilität zwischen Cocoa-Klassen und ihren C-Pendants verspricht. Es kann also sein, dass sich eine Instanz einer Cocoa-Klasse und ihr C-Pendant unterschiedlich verhalten! Dies ergibt sich dann aus der Dokumentation zum C-Objekt.
Zusammenfassung Sie haben jetzt die wesentlichen Konzepte der Ausgabe von Graphik kennengelernt. Sicherlich werden Sie beim Stöbern in der Dokumentation noch mehr Möglichkeiten erkennen. Sie sollten für ein selbständiges Arbeiten aber fit sein. In der Praxis ist allerdings weniger die umfassende Kenntnis von einzelnen Methoden relevant als vielmehr viel, viel Fleiß, um die Darstellung auf dem Bildschirm schön erscheinen zu lassen. Sie werden auch schon bei Applikationen gleich beim ersten Starten gedacht haben, wie ansprechend diese Ihnen erscheinen. Das ist ein deutlicher Marktvorteil, der allerdings eigenes gestalterisches Talent voraussetzt oder die Einbeziehung eines Gestalters. Dabei können wir Ihnen leider nicht helfen. Die Zusammenarbeit mit C-Technologien ist ein Thema, welches Sie im Verlauf des Buches noch benötigen werden. Allerdings verhält es sich nicht mehr so, dass Apple neue Technologien stets zunächst als C-Framework dem Programmierer zur Verfügung stellt und erst später den Zugriff mittels Objective-C ermöglicht. Vielmehr werden inzwischen Technologien originär mit einer Objective-C-API versehen. 148
Nutzeraktionen: Events und Responder
2
SmartBooks
Objective-C und Cocoa – Band 2
Nutzeraktionen: Events und Responder Im letzten Kapitel hatten wir mit dem Benutzer kommuniziert, indem wir ihm Daten präsentierten. Natürlich muss das auch umgekehrt funktionieren: Der Benutzer muss Daten eingeben können. Davon hatten wir ja schon auf nunmehr fast 1000 Seiten auch rege Gebrauch gemacht, indem wir Views zur Eingabe verwendeten, insbesondere Textfelder, Tableviews usw. Aber auch Aktionen aus Menüs kannten wir schon. Und Sie wissen bereits, dass Aktionen durch eine sogenannte Responder-Chain laufen und dann irgendwo ankommen. Wir hatten uns dazu warme Plätzchen ausgesucht, an denen Sie Ihren Code platzieren durften. Und gerade erst haben wir ja eine Methode -print: implementiert. Soviel also zum Stand der Technik. Zwei Dinge sollten Sie als fortgeschrittener Cocoa-Programmierer allerdings noch beherrschen: Wie Nutzereingaben wie Tastaturdrücke und Mausklicks selbst empfangen werden können. Wenn man ja eine eigene Darstellung in einem View hat, will man in der Regel auch dort den Benutzer etwas machen lassen. Bei uns kommt etwa das Verschieben von Karten mit der Maus in Betracht. Zum anderen wollen wir uns nunmehr doch die Responder-Chain genauer anschauen. Allerdings wird es in aller Regel dabei bleiben, dass weiterhin die in Band I genannten Orte für die Implementierung Ihrer Actionmethoden den richtigen Platz bilden. Sie sollten dieses Kapitel also nicht zum Anlass nehmen, durch eigenes Gefrickel am System von Cocoa vorbeizuprogrammieren.
HILFE Bitte laden Sie von der Webseite das Projekt »Card Game 29« herunter. Es handelt sich um das Projekt aus dem letzten Kapitel, in welchem das TableBaizeView sich nicht mehr in einem Scrollview befindet und an dem auch ansonsten zur Übersichtlichkeit einiges abgespeckt ist. Bei all den Möglichkeiten, dass das System – sei es wegen einer Nutzereingabe, sei es aus eigenem Antrieb – mit Ihrem Programm kommunizieren möchte, stellt sich ein grundlegendes Problem: Sie haben zahlreiche Objekte in Ihrem Programm. Irgendeines dieser Objekte soll auf die Nachricht reagieren. Nur welches? Es muss also einen Mechanismus geben, der für die Verteilung (Dispatch, der nichts mit dem Dispatch von Nachrichten in Objective-C zu tun hat) der Systemnachrichten in Ihrem Programm sorgt. Das Problem dabei ist, dass ganz unterschiedliche Arten von Nachrichten existieren, deren Verteilung man nicht über einen Kamm scheren kann. Denken Sie etwa an einen Mausklick: Hier ist klar, dass irgendwie das View, auf das geklickt wurde, wohl etwas mit der Nachricht des Systems zu tun haben muss. Bei einem Tastendruck sieht die Sache schon anders aus, weil sich der nicht 150
Kapitel 2
Nutzeraktionen: Events und Responder
per se einem bestimmten View zuordnen lässt. Allerdings gibt es hier ja so etwas wie den View, der den Fokus hat, erkennbar an dem hellblauen Ring um ihn herum. Dies ist aber nicht die ganze Geschichte: Denn offenkundig lassen sich Tastendrücke auch als Kürzel für das Menü verwenden. Das Menü ist aber nun wirklich eine ganz andere Baustelle als ein View, welches den Fokus hat … Apropos Menü: Ein Klick auf einen Menüeintrag kann einen View betreffen – denken Sie an Einträge wie Format | Schrift | Fett – aber auch in einem Windows-Controller landen. Letzteres hatten wir ja auch schon implementiert. (Und im letzten Kapitel hatten wir etwa das -print: wiederum in einem View untergebracht.) Auch hier scheint es also ganz unterschiedliche Orte der Implementierung zu geben. Langer Rede kurzer Sinn: Es muss mehr als eine Strategie zur Verteilung von Systemnachrichten geben und jede einzelne Strategie scheint nicht ganz einfach zu sein.
AUFGEPASST In diesem Kapitel geht es lediglich darum, Benutzeraktionen, also echte Events, vom System abzuholen und zu behandeln. Noch genauer: Eigentlich sind diese schon abgeholt und müssen nur noch verteilt und abgearbeitet werden. Wir sind also auf der Entnahmeseite. Jenseits muss es natürlich jemanden geben, der etwas in diese Event-Loop hereinlegt. Das ist nur selten der Applikationsentwickler. Tatsächlich verbirgt sich hinter dem Begriff Event-Loop aber noch viel mehr, ja sogar ganz andere Ereignisse. Wir wandern dann von dem Begriff der Event-Loop zur Run-Loop, die alles Mögliche an Eingaben verarbeiten kann und tatsächlich nicht ganz einfach zu verstehen ist. Was sich genau dahinter versteckt, werden Sie in dem Kapitel über Nebenläufigkeit lernen.
Events im System von Cocoa Bei Eintreffen einer Nutzerinteraktion wird zunächst sehr früh eine Instanz der Klasse NSEvent erzeugt. Diese wird dann in der Applikation verteilt. Im Einzelnen:
Events und Responder Die Instanzen der Klasse NSEvent verfügen über verschiedene Eigenschaften. Gemein ist ihnen, dass die Eigenschaft type die Art des aktuellen Events (linke Maustaste gedrückt, Maus bewegt, [Umschalttaste] gedrückt usw.) angibt. Daneben tritt ferner als allgemeine Eigenschaft ein Zeitstempel (Time-Stamp). Je nach Event-Typ können weitere Eigenschaften gesetzt sein, wie die Mausposition bei Mausevents oder der Tastencode bei Tastenevents. 151
SmartBooks
Objective-C und Cocoa – Band 2
Grundsätzlich werden Events von sogenannten Respondern behandelt. Die Basisklasse NSResponder kennt die Subklassen NSApplication, NSWindow und NSDrawer (die bei Fenstern ausziehbaren Schubladen), NSWindowController und NSViewControler sowie NSView und das entsprechende Subklassensystem. Instanzen von all diesen Objekten sind taugliche Empfänger von Events.
Event-Dispatch Zunächst ist darauf hinzuweisen, dass Ausgangspunkt stets eine Nachricht an die Applikation ist, dass ein Event eingetroffen ist. Als Nächstes wird dann die Methode -sendEvent: (NSApplication) aufgerufen. Grundsätzlich leitet die Applikation das Event einfach an das zuständige Fenster weiter, wo ebenfalls die Methode -send Event: existiert. Wir werden aber auch Feinheiten kennenlernen. Dieser Mechanismus ist übrigens fest codiert, so dass eine Abweichung nur möglich ist, wenn die Methode -sendEvent: in einer Ableitung von NSApplication überschrieben wird. Man muss also eigentlich zwei Stufen unterscheiden: Zunächst erfolgt ein Dispatch in -sendEvent: (NSApplication), um Spezialfälle zu beachten. Liegt kein Sonderfall vor, so wird das Event in der Regel an das zum Event in Bezug stehende Fenster weitergeleitet. »In Bezug stehen« bedeutet hierbei nicht, dass es sich um das Fenster im Vordergrund handeln muss. Wir werden das bei den Tasten-Events noch genauer sehen. Aber bei Mausklicks dürfte Ihnen schon klar sein, dass dasjenige Fenster in Bezug zum Event steht, auf welches geklickt wurde, das also das in Z-Richtung oberste Fenster ist, welches unter den Bildschirmkoordinaten des Mausklicks liegt.
AUFGEPASST Jetzt überkommt uns der Beschützerinstinkt: Sowohl NSWindow als vor allem auch NSApplication sollten in der Regel nicht abgeleitet werden, da sie umfangreiche Delegates haben. Verwenden Sie diese zur Anpassung des Verhaltens. Die Änderung des Event-Dispatches ist allerdings ein Fall, in dem man zur Subklasse greift. Das muss man nur so gut wie nie machen. Bevor Sie also wild eigene Applikationsklassen programmieren, fragen Sie sich dreimal, ob Ihr Problem nicht über ein Delegate gelöst werden kann. Das Fenster wiederum macht zweierlei Dinge: Zum einen übersetzt es das Event in verschiedene Nachrichten. So wird das Event für einen Mausklick etwa in die Nachricht -mouseDown übersetzt. Für die wichtigsten behandelten Events ergibt sich folgende Tabelle:
152
Kapitel 2
Nutzeraktionen: Events und Responder
Event-Typ
Methode
Klicks
Linke Taste
-mouseDown:, -mouseUp:, -mouseDragged
Rechte Taste
-rightMouseDown:, -rightMouseUp:, -rightMouseDragged:
Andere Taste
-otherMouseDown:, -otherMouseUp:, -otherMouseDragged:
Mausbewegung Mausbewegung
-mouseMoved:, -mouseEntered, -mouseExited:, -cursorUpdate:
Scrollrad
-scrollWheel:
Tasten (Lebende) Taste
-keyDown:, -keyUp:
(Stumme) Umschalttaste -flagsChanged: Graphiktablett Annäherung
-tabletProximity
Stiftänderung
-tabletPoint
Gesten Gestenzyklus
-beginGestureWithEvent:, -endGestureWithEvent:
Vergrößerung
-magnifyWithEvent:
Drehung
-rotateWithEvent:
Wischen
-swipeWithEvent
Touches Touchzyklus
-touchesBeginWithEvent:, -touchesEndedWithEvent, -touchesCancelledWithEvent:
Bewegung
touchesMovedWithEvent:
AUFGEPASST Seit Mac OS X 10.5.2 sind Gesten und Touches auch für Macs (Desktops und Notebooks) verfügbar. Allerdings kann man sich als Programmierer nicht darauf verlassen, dass der Nutzer über ein entsprechendes Eingabegerät verfügt. Daher muss jede Funktion auch anders erreichbar sein. Sie können allerdings gut für vereinfachte Eingabemöglichkeiten von Operationen wie Vergrößerung dienen.
153
SmartBooks
Objective-C und Cocoa – Band 2
Beachten Sie bitte, dass nicht alle Events stets geschickt werden, um das Laufzeitverhalten zu verbessern. Ein linker Mausklick mit gedrückter [ctrl]-Taste wird übrigens nicht automatisch zum rechten Mausklick. Damit Sie Other-Mouse-Events empfangen können, müssen Sie in den Systemeinstellungen die Taste als Taste 4 anmelden. Sie lernen daraus schon, dass eine Applikation auch ohne diese weiteren Maustasten bedienbar sein sollte. Sonst greifen Sie mittelbar in die Systemeinstellungen des Benutzers ein.
Damit die Klicks bei uns ankommen, müssen wir Sonderfunktionen in den Systemeinstellungen aktivieren.
Als Zweites sucht das Fenster nach dem in Bezug genommenen View. Bei einem Mausklick wäre dies etwa das View, auf das geklickt wurde. Dieses erhält dann die obige Nachricht.
GRUNDLAGEN Wenn es um ein Tastenevent geht, dann ist der in Bezug genommene View der sogenannte First-Responder des Key-Windows, also desjenigen mit dem Focus-Ring. Ähnliches gilt für Actionmethoden. Bei einem Mausklick ergibt sich die Zuordnung zum View aus der Position der Maus. Man kann dies übrigens auch recht leicht selbst nachschauen, was wir hier testweise machen wollen. Hierzu überschreiben wir die beiden Methode -sendEvent: von 154
Kapitel 2
Nutzeraktionen: Events und Responder
NSApplication und NSWindow in Subklassen. Fangen wir zunächst mit dem einfacheren NSWindow an: Erzeugen Sie eine neue Subklasse EventWindow, bei der Sie als Vorlage Objective-C class verwenden. Diese passen Sie zunächst an: @interface EventWindow : NSWindow { } @end
Und in der Implementierung @implementation EventWindow - (void)sendEvent:(NSEvent*)event { NSLog( @"Event in Window: %@", event [super sendEvent:event]; } @end
);
Wir geben hier also nur aus, dass die Methode -sendEvent: (NSWindow) erreicht wurde, und fügen dann die dazu gehörige Event-Description an. Damit unsere neue Fensterklasse auch benutzt wird, müssen Sie jetzt in MainMenu.xib entsprechend im Identity-Pane des Inspectors die Klasse eintragen. Ähnlich bereiten wir jetzt eine Subklasse von NSApplication vor: Erzeugen Sie eine Klasse wiederum von der Vorlage Objective-C class, die Sie EventApplication nennen. Im Header ändern Sie wieder die Basisklasse: @interface EventApplication : NSApplication { } @end
Und fügen Sie eine Implementierung ein: @implementation EventApplication - (void)sendEvent:(NSEvent*)event { NSLog( @"Event in Application: %@", event ); [super sendEvent:event]; } @end
155
SmartBooks
Objective-C und Cocoa – Band 2
Bedenken Sie aber, dass gleich zu Anfang der Anwendung automatisch eine Instanz der Applikationsklasse erzeugt wird. Wir müssen daher noch mitteilen, dass nicht mehr die Basisklasse NSApplication verwendet werden soll, sondern unsere Subklasse. Dies lässt sich in Xcode für den Target über das Info-Window einstellen. Einfach das Target anwählen und [Befehl]+[I] drücken:
Man kann die Klasse der Startinstanz in der Target-Info einstellen.
Übersetzen und starten Sie bitte das Programm. Noch bevor etwas passiert, erreichen schon die ersten Events die Applikationen. Klicken Sie einmal in das Fenster oder verschieben Sie es. Sie werden bei genauer Betrachtung sehen, dass manche Events nicht an unser Fenster gelangen. Dies kann unterschiedlich Gründe haben:
•
Es handelt sich um spezielle Events der Kategorie AppKit-Defined oder System-Defined. Diese werden von NSApplication gemeinsam mit Cocoa bearbeitet. Sie haben die Möglichkeit, auf entsprechende Ereignisse über das Delegate zu reagieren. So existiert etwa ein Event dafür, dass die Applikation aktiviert wurde – und die entsprechenden Delegatemethoden -applicationWillBecomeActive: bzw. -applicationDidBecomeActive:.
•
Das Event ist für andere Fenster bestimmt: So bilden etwa die Menüs auch nur Views, die in Fenstern sind – wie alles Gezeichnete auf dem Schirm. Im Kapitel über Fenster werden wir auch mal selbst extravagante Varianten von Fenstern erzeugen.
156
Kapitel 2
Nutzeraktionen: Events und Responder
Insgesamt dient dies aber wie gesagt nur unserer Forschungsarbeit. Dass Sie wirklich am Event-Dispatching eine Änderung vornehmen wollen, ist eher fernliegend.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 30« von der Webseite herunterladen. Da Sie schnell den Wald vor lauter Bäumen nicht mehr sehen würden, setzen Sie wieder in der Target-Info NSApplication als Principal-Class. Sie können dann auch die neuen Klassen aus dem Projekt entfernen.
Eventmethode und Responder-Chain Nachdem also das passende Fenster gefunden wurde, schickt es die ausgewählte Eventnachricht an das zuständige View. Dabei sind diese Methoden in der Basisklasse NSResponder bereits implementiert. Allerdings macht diese Basisimplementierung nichts anderes, als die Methode an den Next-Responder weiterzuleiten. Wenn man also selbst das Event bearbeiten möchte, so muss man in einer Subklasse von NSResponder, insbesondere bei NSWindow oder NSView, die entsprechende Eventmethode überschreiben und das Event nicht weiterleiten.
AUFGEPASST Es kann durchaus sein, dass zwar grundsätzlich Events der vorliegenden Art vom Responder abgearbeitet werden sollen, jedoch nicht das aktuelle. Denken Sie hier etwa daran, dass ein View lediglich ganz bestimmte Tastendrücke behandeln will. In diesem Falle muss das nicht abgearbeitete Event weitergeleitet werden. Dies geschieht durch Aufruf der Superimplementierung. Wir machen das später auch noch. Die Verkettung vom ersten Empfänger einer Eventnachricht über die Kette der Next-Responder bis hin zum letzten Glied nennt man die Responder-Chain. Findet sich bis zum Ende kein Abnehmer, so wird -noResponderFor: (NSResponder) ausgeführt, welches standardmäßig für einen Tastendruck einen Beep-Ton ausgibt, um den Benutzer davon zu unterrichten, dass sein Tastendruck keine Tätigkeit auslöste. Dies kennen Sie wahrscheinlich, und es wird auch gleich von uns genauer betrachtet. Bei anderen Events wird dieses einfach weggeschmissen.
157
SmartBooks
NSApplication aktivesFenster
-sendEvent:
-sendEvent:
OS X/Cocoa
Objective-C und Cocoa – Band 2
NSWindow Bezogenes View -eventMethod: NSResponder nextResponder
-eventMethod: … -noResponderFor:
NSResponder nextResponder
Die Grundstruktur des Event-Dispatches
Wie bereits gesagt, sucht das Fenster nach einem passenden View. Es dürfte auf der Hand liegen, dass dies bei einem Mausklick dasjenige View ist, auf welches geklickt wurde. Zunächst nur zu Testzwecken richten wir hier mal eine Eventmethode -mouseDown: ein, die uns nur die Responder-Chain ausgibt. In CardStackView.m: - (void)logChain:(NSEvent*)event { id responder = self; do { NSLog( @"Responder Class: %@", NSStringFromClass( [responder class] ) ); } while( (responder = [responder nextResponder]) ); } - (void)mouseDown:(NSEvent*)event { [self logChain:event]; [super mouseDown:event]; } - (id)initWithFrame:(NSRect)frame {
158
Kapitel 2
Nutzeraktionen: Events und Responder
Hierin geben wie nur kurz die Responder-Chain aus und führen dann die Basisimplementierung aus, leiten also einfach die Nachricht an den nächsten Responder weiter, so dass sie durch die Responder-Chain fällt – bis eben jemand die Nachricht abgreift und nicht mehr weiterleitet. Bitte probieren Sie das aus, indem Sie das Programm starten und dann auf einen Stapel klicken: >… Event in Window: NSEvent: type=LMouseDown loc=(576,433) time=76945.2 flags=0x100 win=0x0 winNum=5687 ctxt=0x12387 evNum=4433 click=1 buttonNumber=0 pressure=1 >… Responder Class: CardStackView >… Responder Class: TableBaizeView >… Responder Class: NSView >… Responder Class: EventWindow
Die Eventmethode wird also zunächst durch die View-Hierarchie nach oben getragen, bis sie letztlich beim Fenster landet. Da wir das Event nicht abgreifen, können wir den Lauf durch die Responder-Chain verfolgen. In TableBaizeView.m: - (void)mouseDown:(NSEvent*)event { NSLog( @"in %@", NSStringFromClass( [self class] ) ); [super mouseDown:event]; } - (id)initWithFrame:(NSRect)frame {
und in EventWindow.m: - (void)mouseDown:(NSEvent*)event { NSLog( @"in %@", NSStringFromClass( [self class] ) ); [super mouseDown:event]; } @end
159
SmartBooks
Objective-C und Cocoa – Band 2
Wenn Sie wieder auf einen Kartenstapel klicken, sehen Sie es: >… >… >… >… >… >…
Responder Class: CardStackView Responder Class: TableBaizeView Responder Class: NSView Responder Class: EventWindow in TableBaizeView in EventWindow
HILFE Sie können das Projekt in diesem Zustand als »Card Game 31« von der Webseite herunterladen. Gut, dies sind die Grundzüge des Dispatches. Kommen wir zu den Feinheiten und den verschiedenen Eventmethoden.
160
Kapitel 2
Nutzeraktionen: Events und Responder
Mausevents Mausevents sind recht leicht zu verstehen, weil sie auf ein View gerichtet sind. Allerdings gelten ein paar Besonderheiten für die Abfolge der aufgerufenen Eventmethoden.
Dispatch Wie bereits gezeigt, landen diese ganz einfach in dem View, auf dem sich der Mauszeiger befand, als die Maustaste gedrückt wurde. Etwas anderes ergibt sich nur dann, wenn sich das Fenster im Hintergrund befand und der Mausklick es nach vorne holt. In diesem Falle aktiviert sich das Fenster und wirft das Event weg. Probieren Sie dies bitte aus: Sie werden erkennen, dass nach dem Eingang des Events -sendEvent: (NSWindow) keine Weiterleitung erfolgt. Klick in das inaktive Fenster: >… Event in Window NSEvent: type=LMouseDown loc=(398,233) time=1235284.8 flags=0x100 win=0x0 winNum=40414 ctxt=0x1a727 evNum=-15186 click=1 buttonNumber=0 pressure=1 >… Event in Window NSEvent: type=LMouseUp loc=(398,233) time=1235284.9 flags=0x100 win=0x0 winNum=40414 ctxt=0x1a727 evNum=-15186 click=1 buttonNumber=0 pressure=0
Dann bei einem weiteren Klick in das inzwischen aktive Fenster: >… Event in Window NSEvent: type=LMouseDown loc=(398,233) time=1235286.1 flags=0x100 win=0x0 winNum=40414 ctxt=0x1a727 evNum=-15185 click=1 buttonNumber=0 pressure=1 >… Responder Class: CardStackView >… Responder Class: TableBaizeView >… Responder Class: NSView >… Responder Class: EventWindow >… in TableBaizeView >… in EventWindow >… Event in Window NSEvent: type=LMouseUp loc=(398,233) time=1235286.1 flags=0x100 win=0x0 winNum=40414 ctxt=0x1a727 evNum=-15185 click=1 buttonNumber=0 pressure=0
161
SmartBooks
Objective-C und Cocoa – Band 2
Sie sehen also, dass der erste Klick vom – in diesem Moment noch inaktiven – Fenster verschluckt wird. Für einen Klick mit der rechten Maustaste verhält es sich übrigens – entgegen der Dokumentation von Apple – so, dass keine Aktivierung erfolgt, sondern das Event auf die übliche Weise ausgeliefert wird. Wenn man bei einem Klick mit der linken Maustaste neben der Aktivierung gleich die entsprechende Funktion ausführen möchte, also gleichermaßen »in das Fenster reinklicken« (Click-Through) will, so muss man die Methode -acceptsFirstMouse: (NSView) in seiner Ableitung überschreiben. Dies machen wir mal in CardStackView.m: - (BOOL)acceptsFirstMouse:(NSEvent*)event { return YES; } - (void)mouseDown:(NSEvent*)event
Sie werden nunmehr bemerken, dass bereits der erste Klick durch die ResponderChain läuft. Hiervon machen etwa Buttons Gebrauch, so dass man diese unmittelbar anklicken kann, ohne erst vorher das Fenster aktivieren zu müssen. Kommentieren Sie jetzt bitte in EventWindow.m die Methode -sendEvent: aus, damit der Log nicht überflutet wird. Löschen sollten Sie diese Methode jedoch nicht. Wenn Sie die Kommentierung wieder wegnehmen, können Sie bei Bedarf die einzelnen Events anschauen. /* - (void)sendEvent:(NSEvent*)event { NSLog( @"Event in Window: %@", event [super sendEvent:event]; } */
162
);
Kapitel 2
Nutzeraktionen: Events und Responder
Eventmethoden Man kann die Mausmethoden in verschiedene Gruppen stopfen:
•
Klick-Methoden, die das Drücken und Loslassen der Maustaste anzeigen, sowie Drag-Methoden, die die Bewegung der Maus bei gedrückter Maustaste übermitteln.
•
Move-Methoden, die die reine Bewegung der Maus oder das Betreten und Verlassen von Bereichen mitteilen.
• •
Die Betätigung des Scroll-Wheels. Die Klick- und Drag-Methoden existieren dreifach: für die linke Maustaste, für die rechte Maustaste (dann muss dem Methodennamen ein »right« vorangesetzt werden) und für weitere Tasten (other). Wir werden hier in den Beispielen lediglich die linke Maustaste verwenden. Durch Voransetzen der jeweiligen Präfixe können Sie aber auch entsprechende Experimente mit den anderen Maustasten unternehmen.
Klick-Methoden Bei Klick-Methoden ist es wichtig, den Ablauf der einzelnen Events bei verschiedenen Nutzeraktionen zu kennen. Um dies auszuprobieren, bohren wir CardStackView.m ein wenig auf: - (void)mouseDown:(NSEvent*)event { NSLog( @"LTaste unten Klicks: %d", [event clickCount] ); } - (void)mouseUp:(NSEvent*)event { NSLog( @"LTaste oben Klicks: %d", [event clickCount] ); } - (void)mouseDragged:(NSEvent*)event { NSLog( @"LTaste drag Klicks: %d", [event clickCount] ); }
163
SmartBooks
Objective-C und Cocoa – Band 2
Einfachklicks Wenn Sie das Programm starten und auf einen Kartenstapel klicken, dann sehen Sie entsprechend die ausgeführten Methoden: >… LTaste unten Klicks: 1 >… LTaste oben Klicks: 1
Gut, das war jetzt wahrscheinlich nicht überraschend. Mehrfachklicks Um also Mehrfachklicks, insbesondere Doppelklicks zu unterstützen, muss man einfach die Anzahl der Klicks abfragen. Probieren wir das aus: - (void)mouseDown:(NSEvent*)event { switch( [event clickCount] ) { case 1: NSLog( @"LEinfach+" ); break; case 2: NSLog( @"LDoppelt+" ); break; default: NSLog( @"L%d-fach+", [event clickCount] ); break; } } - (void)mouseUp:(NSEvent*)event { switch( [event clickCount] ) { case 1: NSLog( @"LEinfach-" ); break; case 2: NSLog( @"LDoppelt-" ); break; default: NSLog( @"L%d-fach-", [event clickCount] ); break; } }
164
Kapitel 2
Nutzeraktionen: Events und Responder
Wenn Sie jetzt einen Doppelklick ausführen, wird sich vielleicht Enttäuschung breit machen: >… >… >… >…
LEinfach+ LEinfachLDoppelt+ LDoppelt-
Jeder Doppelklick wird als Einfachklick geboren.
Dies ist dann schon etwas überraschend: Der Doppelklick führt zunächst zu einem einfachen Klick und dann zu einem Doppelklick. (Wenn Sie übrigens einen Dreifachklick ausführen, so verlängert sich entsprechend die Liste.) Da stellt sich schon die Frage, wieso das so ist und warum nicht einfach ein Doppelklick gemeldet wird. Der Hintergrund ist ganz einfach: Beim ersten Klick kann das System noch nicht wissen, ob ein zweiter folgt. Es hat daher zwei Möglichkeiten:
•
Es wartet zunächst die Doppelklickzeit ab, ob noch ein Klick kommt. Ist dies nicht der Fall, so wird ein Einfachklick gemeldet.
•
Es meldet sofort schon einmal den Einfachklick. Sollte innerhalb der Doppelklickzeit ein weiterer Klick folgen, so wird eben dieser als Doppelklick zusätzlich gemeldet.
Wie Sie sehen, haben wir es mit der zweiten Methode zu tun. Und das ist auch gut so: Würde das System bei jedem Klick abwarten, ob noch einer kommt, dann wäre die Reaktionszeit des User-Interfaces extrem verlangsamt. Location Zu einem Mausevent gehört freilich auch der Ort des Geschehens. Dieser wird als Eigenschaft locationInWindow der NSEvent-Instanz mitgeliefert. Wie der Name schon verrät, handelt es sich dabei nicht um die Position des Klicks im View, sondern im Fenster. Es ist daher umzuwandeln. Dazu dient die Methode -convertPoint:fromView: (NSView), welche in der Lage ist, von einem Koordinatesystem eines Views in demselben Fenster zu einem anderen umzurechnen. Wird 165
SmartBooks
Objective-C und Cocoa – Band 2
als das andere View nil übergeben, so erfolgt eine Umrechnung aus dem Koordinatensystem des Fensters. Bauen wir etwas mehr oder weniger Sinnvolles in unser Programm ein, um all dieses neue Wissen anzuwenden: Wenn auf eine Karte geklickt wird, soll diese als selektiert markiert und angezeigt werden. Dazu müssen wir uns zunächst in einer Instanzvariablen die aktuelle Selektion merken, wobei wir jedoch gleich berücksichtigen, dass mehrere Karten selektiert sein könnten. Aus diesem Grunde speichern wir die Selektion als Instanz der Klasse NSIndexSet, wobei die einzelnen Indexe die Karten bezeicnen: @interface CardStackView : NSView { … NSIndexSet* selectedCardsIndices; } … @property( copy ) NSIndexSet* selectedCardsIndices;
Hierzu schreiben wir uns selbst ein Accessorenpaar, welches wir allerdings ausprogrammieren, da wir eine automatische Aktualisierung des Views erreichen wollen. Wir wenden hier aus Gründen der Einfachheit die dumme Variante der Aktualisierungsmarkierung an: @implementation CardStackView @synthesize alignment; - (void)setSelectedCardsIndices:(NSIndexSet*)value { if( value != selectedCardsIndices ) { [selectedCardsIndices release]; selectedCardsIndices = [value retain]; } [self setNeedsDisplay:YES]; } - (NSIndexSet*)selectedCardsIndices { return selectedCardsIndices; }
166
Kapitel 2
Nutzeraktionen: Events und Responder
Entsprechend müssen wir -initWithFrame: und -dealloc anpassen: - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { … self.selectedCardsIndices = [NSIndexSet indexSet]; } return self; } - (void) dealloc { self.selectedCardsIndices = nil; … }
Unser -mouseDown: erledigt dann das Setzen der entsprechenden Eigenschaft, -mouseUp: und -mouseDragged: leeren wir: // Hilfsfunktion - (NSInteger)indexOfCardAtLocation:(NSPoint)location { NSUInteger cardCount = [self.cards count]; NSInteger cardIndex; for( cardIndex = cardCount-1; cardIndex > -1; cardIndex-- ) { NSRect cardRect = [self rectForCardWithIndex:cardIndex count:cardCount getIsJammed:NULL]; if( NSPointInRect( location, cardRect ) ) { // gefunden break; } } return cardIndex; }
167
SmartBooks
Objective-C und Cocoa – Band 2
- (void)mouseDown:(NSEvent*)event { NSInteger cardIndex; // Bei leerem View nichts tun if( [self.cards count] == 0 ) { return; } // Mausposition auf den aktuellen View beziehen NSPoint location = [event locationInWindow]; location = [self convertPoint:location fromView:nil]; cardIndex = [self indexOfCardAtLocation:location]; // Treffer markieren if( cardIndex < 0 ) { return; } self.selectedCardsIndices = [NSIndexSet indexSetWithIndex:cardIndex]; } - (void)mouseUp:(NSEvent*)event {} - (void)mouseDragged:(NSEvent*)event {}
Damit wir auch etwas sehen, müssen wir beim Zeichnen des Views nunmehr die Selektion berücksichtigen. Wir stellen dies als blaue Hintergrundfarbe dar. Wenn Sie in Ihren Systemeinstellungen eine andere Selektionsfarbe gewählt haben, so wird freilich diese genommen – Und das ist auch gut so (Wowereit). Zum einen ändern wir dafür die Methode zum Zeichnen des Hintergrundes - (void)drawCardBorderInRect:(NSRect)rect selected:(BOOL)selected { … // Selektierte Karten: if( selected ) { [[NSColor selectedControlColor] setFill]; } else { [[NSColor whiteColor] setFill];
168
Kapitel 2
Nutzeraktionen: Events und Responder
} [path fill]; … }
zum anderen deren Aufruf: - (void)drawRect:(NSRect)rect { … for( index = 0; index < count; index++ ) { … if( [self needsToDrawRect:drawRect] ) { BOOL selected = [self.selectedCardsIndices containsIndex:index]; [self drawCardBorderInRect:cardRect selected:selected]; … } } }
Bitte testen Sie das Programm. Sie sollten jetzt in jedem Stapel jeweils eine Karte selektieren können, die entsprechend blau gemalt wird.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 32« von der Webseite herunterladen. Modifier-Flags Kümmern wir uns um die Mehrfachauswahl: Die gelieferte Event-Instanz verfügt über eine Eigenschaft modifierFlags, welche uns die gedrückten Umschalttasten ([Feststelltaste], [Umschalttaste], [Controltaste], [Optionstaste], [Befehlstaste]) mitteilt. Wir verwenden das, um mehrere Karten selektieren zu können bzw. wieder welche zu deselektieren. Da mehrere Tasten gleichzeitig gedrückt sein können, wird der Status als Bitpattern mitgeteilt. Sie können dazu Genaueres in der Referenz nachlesen. Hier sei darauf verwiesen, wie dies grundsätzlich geprüft wird: - (void)mouseDown:(NSEvent*)event {
169
SmartBooks
Objective-C und Cocoa – Band 2
… // Treffer markieren if( cardIndex < 0 ) { return; } NSMutableIndexSet* selections = [[[NSMutableIndexSet alloc] initWithIndexSet:self.selectedCardsIndices] autorelease]; NSUInteger modifiers = [event modifierFlags]; // Falls Befehlstaste gedrueckt ist: Auswahl umkehren if( modifiers & NSCommandKeyMask ) { if( [selections containsIndex:cardIndex] ) { [selections removeIndex:cardIndex]; } else { [selections addIndex:cardIndex]; } // Kein Modifier: Karte als einzige Selektion setzen } else { selections = [NSMutableIndexSet indexSetWithIndex:cardIndex]; } self.selectedCardsIndices = selections; }
170
Kapitel 2
Nutzeraktionen: Events und Responder
Wie Sie sehen, kann man eine einzelne Taste mittels des Bit-Und-Operators & abfragen. Sie können jetzt bei gedrückter Befehlstaste einzelne Karten hinzufügen bzw. wieder entfernen. Insgesamt stehen folgende Kostanten zur Verfügung: Konstante
Aufdruck
Beschreibung
NSAlpheShiftKeyMask
u
Feststelltaste
NSShiftKeyMask
s
Umschalttaste
NSControlKeyMask
ctrl
Controltaste
NSAlternateKeyMask
alt o
Optionstaste
NSCommandKeyMask
cmd c
Befehlstaste
NSNumericPadKeyMask
0 bis 9 , + - * / = P
Ziffernblock
fn
Funktionsumschalttaste
NSHelpKeyMask NSFunctionKeyMask
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 33« von der Webseite herunterladen.
Drag-Methoden Die Drag-Methoden sind eine Kombination zwischen den Klickmethoden und den Bewegungsmethoden. Ein solches Ereignis wird nur erzeugt, wenn bei gedrückter Maustaste die Maus bewegt wird.
Nur wenn die Maustaste gedrückt ist, werden Drag-Nachrichten erzeugt.
171
SmartBooks
Objective-C und Cocoa – Band 2
Die Bedeutung des Drags Selektion
Einfügen Häufig hängt die Funktion eines Drags von einer Einstellung ab. Hier am Beispiel von OmniGraffle.
Üblicherweise implementiert man -mouseDragged: mit verschiedenen Zielen, die wir mal benannt haben:
•
Insert-Drag: Der Nutzer fügt ein Element dem View hinzu, zieht etwa eine Linie vom Startpunkt des Drags zum Endpunkt des Drags oder zieht einen Kreis auf, ein Rechteck usw. Dies ist häufig in graphisch orientierten Programmen der Fall. Andere (Apples Keynote) handhaben diese Aktion so, dass das gewünschte neue Element über eine Action einfach in die Zeichnung eingefügt wird und später mittels Dragging verschoben und vergrößert werden kann:
Ein Insert-Drag im Programm OmniGraffle
•
Edit-Drag: Wir haben einen Anfasspunkt an einem Element, den der Nutzer durch einen Drag verändern kann, insbesondere vergrößern. Dies können Sie 172
Kapitel 2
Nutzeraktionen: Events und Responder
etwa im Interface Builder sehen, wenn Sie ein Element selektieren und dann in der Größe mit Hilfe der kleinen Kügelchen als Anfasspunkte verändern.
Mit dem Edit-Drag im Interface Builder können wir die Ausmaße eines Views ändern.
•
Move-Drag: Ebenfalls hierher gehören diejenigen Fälle, in denen Sie zwar ein Objekt im View verschieben können, das Objekt jedoch nicht den View verlassen kann. In manchen Graphikprogrammen ist es etwa so, dass ein Element, welches gezogen wird, nur innerhalb des Dokumentes neu platziert werden kann.
Verschoben, aber nur innerhalb des Views: ein Move-Drag
•
Selection-Drag: Der Nutzer soll in die Lage versetzt werden, sämtliche Elemente in einem Bereich zu selektieren. Dies erfolgt etwa im Interface Builder, wenn Sie mehrere Elemente in einem Fenster auswählen können.
173
SmartBooks
Objective-C und Cocoa – Band 2
Mit einem Selection-Drag kann man mehrere Objekte auf einen Schlag selektieren.
•
Drag & Drop: Es wird eine Verschiebe- oder Kopieraktion durchgeführt. Dies bedeutet, dass ein Element aus einem View in ein anderes gezogen werden kann, sein View also verlässt. Ein Beispiel hierfür ist wieder der Interface Builder, bei dem Elemente aus einem Fenster in das Hauptfenster gezogen werden können.
Ist der Drag ein Bestandteil eines Drag-&Drop-Zyklusses, so kann das Objekt aus dem View herausgeschoben werden.
174
Kapitel 2
Nutzeraktionen: Events und Responder
•
Diese Liste lässt sich anhand konkreter Applikationen erweitern, wie etwa scrollen, zoomen usw.
Sie mögen die verschiedenen Operationen anhand von Gemeinsamkeiten gruppieren können – und liegen damit vermutlich falsch: Tatsächlich ist es so, dass die ersten vier Drags (…-Drag) in eine Gruppe gehören und der letzte Fall (Drag & Drop) etwas ganz anderes ist. Der Hintergrund ist, dass in der ersten Gruppe die Aktion komplett von unserem View gehandhabt wird, weil sie vollständig in unserem View stattfindet. Dagegen ist es bei dem Drag & Drop so, dass mehrere Views, sogar mehrere Fenster, ja sogar mehrere Applikationen an dem Zyklus beteiligt sind. Denken Sie etwa an den Fall, dass Sie einen Text aus TextEdit auf den Desktop ziehen: von TextEdit zum Finder. Hier können wir also gar nicht selbst das Dragging abhandeln, sondern müssen es an das System delegieren. Deshalb haben wir diesen Fall auch in einen eigenen Abschnitt behandelt. Was aber auch immer mit einem Drag geschehen soll, so muss das Programm bei Empfang der Nachricht mouseDragged: entscheiden, welche Aktion eingeleitet werden soll. Das hängt natürlich in hohem Maße von Ihrer Applikation ab. Dennoch gibt es ein paar Regeln, deren Einhaltung der Nutzer erwartet:
•
Wenn im »Hintergrund« eine Dragoperation gestartet wird, so erfolgt je nach Modus der Applikation entweder eine Selektion der im aufgezogenen Rechteck liegenden Elemente (Selection-Drag) oder es wird ein neues Element im aufgezogenem Rechteck eingefügt (Insert-Drag).
•
Beginnt der Drag auf einem Anfasspunkt eines selektierten Elementes – nur dann werden die Anfasspunkte angezeigt –, so erfolgt eine Vergrößerungsoperation (Edit-Drag).
•
Wenn ein unselektiertes Element Startpunkt des Drags ist, so wird dieses (allein) selektiert und verschoben oder kopiert (Move-Drag, Drag & Drop).
•
Beginnt der Drag auf einem selektierten Element, so wird dieses und alle anderen selektieren Elemente verschoben (Move-Drag, Drag & Drop).
Möglichkeiten der Implementierung Es gibt grundsätzlich zwei Arten, das interne Dragging zu implementieren. Apple nennt diese beiden Ansätze »Three-Methods-Approach« und »Mouse-TrackingLoop-Approach«. Wir finden dies trotz der Langatmigkeit der Bezeichnung etwas ungenau und verwenden daher die Begriffe »unmodal« und »modal« für die beiden Ansätze:
•
Three-Methods-Approach (unmodal): Das Grundkonzept beruht hier darauf, dass ein Drag-Zyklus mit einem mouseDown: beginnt, dann eine Mehrzahl 175
SmartBooks
Objective-C und Cocoa – Band 2
von mouseDragged-Nachrichten gesendet wird, um schließlich mit einem mouseUp: beendet zu werden. Jede dieser einzelnen Nachrichten wird für sich abgearbeitet. Sodann wird wieder ins System zurückgekehrt.
Taste drücken
mouseDown: merkt sich Startpunkt
Maus bewegen
mouseDragged: aktualisiert Endpunkt
…
…
Maus bewegen
mouseDragged: aktualisert Endpunkt
Taste loslassen
mouseUp: schließt Operation ab
Nutzeraktionen
Drag-Rechteck View
Eventmethoden
Unmodal: Die Bearbeitung des Drag-Zyklusses erfolgt in drei Methoden.
•
Mouse-Tracking-LoopApproach (modal): Hier erfolgt die Implementierung lediglich in der Methode -mouseDragged:. Ist einmal ein solches Ereignis eingetreten, so wird in der Methode verblieben, bis die Maustaste wieder losgelassen wird.
Taste drücken mouseDown: Fängt Events in Schleife
Maus bewegen
Drag-Events aktualisieren Endpunkt
… Maus bewegen
Up-Event Beendet Schleife
Taste loslassen
Drag-Rechteck View
Operation abschließen
Nutzeraktionen
Eventmethoden
Modal: Bei einem Mouse-Down werden in der Methode Events gesammelt.
176
Kapitel 2
Nutzeraktionen: Events und Responder
Wir werden im Folgenden die beiden Möglichkeiten einmal implementieren: Unmodale Implementierung Zunächst bemühen wir uns um Dragging als Selektierungsmöglichkeit und implementieren dies unmodal. Wir benötigen eine Eigenschaft, die die Startposition speichert. Dazu erweitern wir den Header von TableBaizeView: @interface CardStackView : NSView { … NSRect dragRect; } … @property( copy ) NSIndexSet* selectedCardsIndices; @property NSRect dragRect;
und synthetisieren die Accessoren @implementation CardStackView @synthesize alignment, dragRect;
In -mouseDown: wird die Startposition des Drag-Zyklusses gemerkt: - (void)mouseDown:(NSEvent*)event { // Bei leerem View nichts tun … // Mausposition auf den aktuellen View beziehen NSPoint location = [event locationInWindow]; location = [self convertPoint:location fromView:nil]; NSRect newDragRect = NSZeroRect; newDragRect.origin = location; self.dragRect = newDragRect; }
177
SmartBooks
Objective-C und Cocoa – Band 2
Da wir hier nur den Selection-Drag implementieren, erübrigt sich eine Abfrage, welcher Modus genutzt wird. Diese sähe beispielhaft so aus: // Modus des Views ermitteln: switch( self.selectedTool ) { // Selection-Drag case selectionTool: // Element getroffen: Verschieben if( index >= 0 ) { // Falls noch unselektiert, selektieren if( [self.selectedIndices containsIndex:index] ) { self.selectedIndices = [NSIndexSet indexSetWithIndex:index]; } // neue Selektion self.dragMode = moveDragMode; // Kein Treffer: Auswaehlen } else { self.dragModus = selectionDragMode; } break; // Insertion-Drag case rectangleInsertionTool: case circleInsertionTool: self.dragMode = insertionDragMode; break; … }
Kommen wir aber wieder zum hiesigen Projekt zurück: Nachdem wir den Drag im -mouseDown vorgemerkt haben, müssen wir ihn im -mouseDrag: fortführen. - (void)mouseDragged:(NSEvent*)event { NSRect newDragRect = self.dragRect; NSPoint point = [event locationInWindow]; point = [self convertPoint:point fromView:nil]; newDragRect.size = NSMakeSize( point.x-newDragRect.origin.x, point.y-newDragRect.origin.y );
178
Kapitel 2
Nutzeraktionen: Events und Responder
self.dragRect = newDragRect; [self setNeedsDisplay:YES]; }
Jetzt wird das Drag-Rechteck zwar schon ganz wunderbar aufgezogen, allerdings müssten Sie uns das einfach glauben. Deshalb zeichnen wir es – und verlassen dabei nicht unser drawRect-Gefängnis! - (void)drawRect:(NSRect)rect { … // Zeichne Auswahlrechteck NSRect normRect = self.dragRect; if( normRect.size.width < 0.0 ) { normRect.origin.x += normRect.size.width; normRect.size.width *= -1.0; } if( normRect.size.height < 0.0 ) { normRect.origin.y += normRect.size.height; normRect.size.height *= -1.0; } if( !NSIsEmptyRect( normRect ) ) { // Rechteck auf View begrenzen normRect = NSIntersectionRect( normRect, self.bounds ); // Rechteck auf Pixel ausrichten normRect = [self convertRectToBase:normRect]; normRect = NSInsetRect( normRect, .5, .5 ); normRect = [self convertRectFromBase:normRect]; NSBezierPath* path; path = [NSBezierPath bezierPathWithRect:normRect]; [path setLineWidth:0.0]; [[NSColor colorWithCalibratedWhite:0.3 alpha:0.3] setFill]; [path fill]; [[NSColor blackColor] setFill]; [path stroke]; } }
179
SmartBooks
Objective-C und Cocoa – Band 2
Zunächst einmal wird das Rechteck normiert, dies bedeutet, dass wir es so ausrichten, dass Breite und Höhe positiv sind. Dies ist erforderlich, weil weder NSIsEmptyRect() noch +bezierPathWithRect: (NSBezierPath) negative Größen akzeptieren. Die Normalisierung kann auch nicht im ursprünglichen Drag-Rechteck erledigt werden, da sonst ja der Anfangspunkt unseres Drags verloren ginge. Dann richten wir das Normrechteck auf den Pixelkooridnaten aus, da das Selektionsrechteck eine Haarlinie erhalten soll. Schließlich zeichnen wir eine transparente Füllung und die Umrandung. Beachten Sie aber, dass der Vorgang unter der Bedingung steht, dass das Rechteck eine Breite und Höhe hat (NSIsEmptyRect()). Außerhalb eines Auswahlzyklusses setzen wir einfach das Rechteck in seiner Ausdehnung auf Null. Womit wir beim letzten Punkt wären: Lässt der Benutzer die Taste wieder los, so muss das Auswahlrechteck wieder verschwinden. Dies erledigen wir in -mouseUp: - (void)mouseUp:(NSEvent*)event { self.dragRect = NSZeroRect; [self setNeedsDisplay:YES]; }
Testen! Das Rechteck sollte jetzt aufgezogen werden können und automatisch wieder verschwinden, wenn Sie die Maustaste loslassen.
HILFE Sie können das Projekt in diesem Zustand als »Card Game 34« von der Webseite herunterladen. Die eigentliche Funktionalität, nämlich das Selektieren von Karten, fehlt hier natürlich noch. Dies findet sein Bewenden einfach darin, dass dies nichts mehr mit dem Dragging zu tun hat und unsere Applikation sich auch untypisch verhält, da sich in unserem Kartenspiel die einzelnen Objekte überdecken, jedoch nur der sichtbare Teil der Karten zur Selektion dienen soll. Vergleichen Sie das mal mit einem Graphikprogramm, in dem auch (teilweise) verdeckte Elemente mitselektiert werden. Dieses typische Szenario lässt sich prinzipiell wie folgt implementieren, wobei es natürlich auf die Struktur der gedachten Applikation ankommt: - (void)mouseDragged:(NSEvent*)event { … self.dragRect = newDragRect;
180
Kapitel 2
Nutzeraktionen: Events und Responder
// Rechteck wieder normalisieren if( newDragRect.size.width < 0.0 ) { newDragRect.origin.x += newDragRect.size.width; newDragRect.size.width *= -1.0; } if( newDragRect.size.height < 0.0 ) { newDragRect.origin.y += newDragRect.size.height; newDragRect.size.height *= -1.0; } // Alle Elemente in dem Auswahlrechteck durchgehen und ggfls. // der Selektion hinzufügen NSMutableIndexSet* selection = [NSMutableIndexSet indexSet]; NSInteger index; NSUInteger count = [self.cards count]; BOOL isJammed; for( index = 0; index < count; index++ ) { NSRect cardRect; cardRect = [self rectForCardWithIndex:index count:count getIsJammed:&isJammed]; // Ueberpruefung, ob das Rechteck des Elementes (teil// weise) im Auswahlrechteck liegt. if( NSIntersectsRect( newDragRect, cardRect) ) { [selection addIndex:index]; } } self.selectedCardsIndices = selection; [self setNeedsDisplay:YES]; }
TIPP Wenn man übrigens NSIntersectsRect() durch NSConatinsRect() ersetzt, so werden nur diejnigen Elemente selektiert, die vom Auswahlrechteck voll umfasst werden. Da wir hier – für ein Kartenspiel untypisch – sämtlich Karten im Rechteck selektieren, beginnen Sie am besten beim Text das Drag-Rechteck an der Kante einer der äußersten Karten. 181
SmartBooks
Objective-C und Cocoa – Band 2
HILFE Sie können das Projekt in diesem Zustand als »Card Game 35« von der Webseite herunterladen. Modale Implementierung Die erwähnte zweite Möglichkeit der Implementierung besteht darin, dass man im -mouseDown: auf künftige Events wartet und so lange die Methode nicht verlässt. Die Applikation blockiert also in dieser Methode. Im Prinzip werden also die drei Ereignisse Maustaste geklickt, Maus bei geklickter Taste bewegt und Maustaste losgelassen in einer einzigen Methode abgehandelt. -mouseDragged: und -mouseUp: benötigen wir daher nicht mehr: - (void)mouseDown:(NSEvent *)event { // Bei leerem View nichts tun if( [self.cards count] == 0 ) { return; } NSRect newDragRect; // Mouse-Down: Starten NSPoint point = [event locationInWindow]; point = [self convertPoint:point fromView:nil]; newDragRect.origin = point; // Gegebenenfalls die erste (angeklickte) Karte selektieren // Mouse-Drag-Loop: Rechteck erzeugen und aktualisieren do { event = [[self window] nextEventMatchingMask: NSLeftMouseUpMask | NSLeftMouseDraggedMask]; NSPoint point = [event locationInWindow]; point = [self convertPoint:point fromView:nil]; newDragRect.size.width = point.x - newDragRect.origin.x; newDragRect.size.height = point.y - newDragRect.origin.y; self.dragRect = newDragRect; if( [event type] == NSLeftMouseDragged ) {
182
Kapitel 2
Nutzeraktionen: Events und Responder
// Hier waeren etwa die Elemente zu selektieren. // Aktualisierung nicht vergessen [self setNeedsDisplay:YES]; } } while( [event type] != NSLeftMouseUp ); // Maus-Up: Beenden self.dragRect = NSZeroRect; [self setNeedsDisplay:YES]; } - (void)mouseUp:(NSEvent*)event {} - (void)mouseDragged:(NSEvent*)event {}
Eigentlich ist diese Schleife recht klar verständlich. Nur eine Sache wundert Sie vielleicht – und das zu Recht: Wieso wird das Selektionsrechteck im View neu gezeichnet? Immerhin verhält es sich ja so, dass wir in unserer Methode festhängen und an keiner Stelle -drawRect: selbst aufrufen – wofür wir Sie auch, täten Sie es, standrechtlich erschießen würden. Das Geheimnis liegt darin, dass dieses Redraw-Pseudoevent durchaus noch bearbeitet wird. Wir können dies auch einmal überprüfen: - (void)drawRect:(NSRect)rect { … if( !NSIsEmptyRect( normRect ) ) { NSLog( @"Redraw während Selektierung" ); … } } … - (void)mouseDown:(NSEvent *)event { … do { NSLog( @"Eintritt in nextEventMatchingMask" );
183
SmartBooks
Objective-C und Cocoa – Band 2
event = [[self window] nextEventMatchingMask: NSLeftMouseUpMask | NSLeftMouseDraggedMask]; NSLog( @"Zurueck aus nextEventMatchingMask" ); … } while( [event type] != NSLeftMouseUp ); … } >… Eintritt in nextEventMatchingMask >… Redraw während Selektierung >… Zurueck aus nextEventMatchingMask
AUFGEPASST Der Hinweis in der Dokumentation, dass andere Events nicht bearbeitet werden, ist so also etwas verwirrend. Allerdings befindet sich die Run-Loop in unserer Schleife in einem besonderen Modus. Was es damit auf sich hat, lernen Sie im Kapitel über Nebenläufigkeit. Hier ist es wichtig, dass auch beim modalen Ansatz für das Dragging weiterhin mit -setNeedsDisplay…: (NSView) das Zeichnen veranlasst werden kann, Sie also nicht unmittelbar zeichnen. Sie haben jetzt also am Beispiel einer Selektion beide Ansätze kennengelernt, mit denen man eine Drag-Operation implementieren kann. Zunächst mag der modale Ansatz besser zu überblicken sein. Man darf aber nicht übersehen, dass dies schon der üblichen GUI-Programmierung zuwiderläuft: Modalität gilt es zu vermeiden. Der wesentliche technische Unterschied ergibt sich daraus, dass die Anwendung weitestgehend blockiert. Das kann ein Vorteil sein, das kann ein Nachteil sein. Dies hängt letzlich von Ihrer Applikation ab. Bewusstmachen muss man es sich in jedem Falle.
184
Kapitel 2
Nutzeraktionen: Events und Responder
Drag & Drop Drag & Drop stellt eine besondere Art des Draggings dar. Wie bereits ausgeführt, ist ein solcher Drag-&-Drop-Zyklus keine private Sache eines Views, sondern muss zwischen verschiedenen Views, möglicherweise in verschiedenen Fenstern, möglicherweise von verschiedenen Applikation funktionieren.
AUFGEPASST Noch einmal zur Klarstellung: Wollen Sie lediglich innerhalb Ihres Views etwas verschieben, so sind Sie beim letzten Abschnitt Drag-Methoden besser aufgehoben. Hier geht es also um das systemweite Drag & Drop als Technologie von Cocoa und OS X. Wir hatten uns auch schon im ersten Band mit Drag & Drop beschäftigt, wobei wir eine Stufe weiter waren: Das von uns verwendete Outlineview bereitete das Drag & Drop vor. Wir implementierten lediglich die Delegates, die bestimmte Anfragen der Views beantworteten. Hier geht es jetzt um die andere Seite: Wir bereiten im View das Drag & Drop vor. Wenn wir dann später das View an die Controllerschicht anbinden, müssen wir auch entsprechende Delegatemethoden anbieten. Drag
Delegate erlaubt/ verbietet
erlaubt/ verbietet
Fenster View
fragt nach Bereitet Drag vor Übergibt an System fragt nach
fragt nach
View
Delegate erlaubt/ verbietet
fragt nach
fragt nach
Das System kommuniziert nur mit Views, diese mit ihren Delegates.
Der Drag-&-Drop-Vorgang lässt sich dabei anhand des MVC-Musters in Ebenen unterteilen: Zum einen ist er selbst lediglich ein visueller Vorgang. Dieser visuelle Vorgang ist Sache des Views. Dann bedeutet es aber auch immer einen Datenaustausch. Dieser Datenaustausch ist eine Frage der Controller (und dahinter des Models). Wir implementieren hier also nur den visuellen Teil. Der »funktionale Teil« wird bei der Anbindung der Views an die Controller besprochen. 185
SmartBooks
Objective-C und Cocoa – Band 2
Fenster
Viewebene Visualisierung
View
Controllerebene Steuerung
Delegate
View
Pasteboard
Delegate
Model-Ebene Model
Die Views sorgen dafür, dass der Nutzer etwas sieht, die Controller unterhalten sich über den Datenaustausch.
AUFGEPASST Zuweilen mag diese Unterteilung künstlich oder kompliziert erscheinen. Aber bedenken Sie, dass die Modellierung sowohl der Regeln als auch der einzelnen Karten ganz unterschiedlich sein kann. Das MVC-Muster hilft daher bei der Wiederverwertbarkeit von Code enorm. Wir legen sehr viel Wert auf die saubere Einhaltung. Ein Verstoß rächt sich früher oder später. Ein kompletter Drag-&-Drop-Zyklus sieht daher wie folgt aus:
•
Der Quellview (Dragging-Source) leitet den Zyklus ein, meist in der Methode -mouseDown: oder -mouseDragged.
•
Das System richtet laufend Anfragen an den Quellview. Hierdurch kann es den Zyklus beeinflussen. Allerdings bedeutet das im MVC-Muster, dass das View wie186
Kapitel 2
Nutzeraktionen: Events und Responder
derum die Anfragen an sein Delegate als Controller weiterleitet. Hierbei kann aber das View die nur das View betreffende graphische Darstellung berücksichtigen.
•
Ebenso richtet das System Anfragen an das potentielle Zielview. Auch hier gilt wegen des MVC-Musters, das, soweit sich die Anfrage aus der graphischen Repräsentierung ergibt (bestimmte Bereiche in die gedroppt werden kann usw.), die Anfragen beantwortet. Dies wäre etwa in unserem Kartenspiel der Umstand, dass man nur unten anlegen kann. Demgegenüber sind die Regeln des Kartenspieles im Controller implementiert. Insoweit muss das View sein Delegate um Erlaubnis bitten.
Drag-Zyklus starten Fangen wir an, das Ganze praktisch umzusetzen. Zunächst löschen wir -mouseDown und -mouseUp:, weil wir den Drag-Zyklus in -mouseDragged: beginnen lassen und diese Methoden nicht mehr benötigen: - (void)mouseDragged:(NSEvent*)event { // Bitte gehen Sie weiter, hier gibt es nichts zu sehen NSUInteger count = [self.cards count]; if( count == 0) { return; } // Image erzeugen // gezogene Karte besorgen NSPoint location = [event locationInWindow]; location = [self convertPoint:location fromView:nil]; NSInteger index = [self indexOfCardAtLocation:location]; // Falls ausserhalb des Stapels: Weg! if( index == -1 ) { return; } // Groesse berechnen NSRect draggedRect = [self rectForCardWithIndex:index count:count getIsJammed:NULL]; NSRect lastRect = [self rectForCardWithIndex:count-1 count:count getIsJammed:NULL];
187
SmartBooks
Objective-C und Cocoa – Band 2
NSRect imageRect = NSUnionRect( draggedRect, lastRect ); // Bild zeichnen NSImage* dragImage = [[[NSImage alloc] initWithSize:imageRect.size] autorelease]; [dragImage lockFocus]; [[NSColor colorWithCalibratedRed:1.0 green:0.0 blue:0.0 alpha:0.8] setFill]; NSRectFill( NSMakeRect( 0.0, 0.0, NSWidth( imageRect), NSHeight( imageRect) ) ); [dragImage unlockFocus]; …
Für einen Drag-&-Drop-Zyklus muss ein Bild zur Verfügung gestellt werden. Dessen Ausmaße bestimmen wir als vereinigtes Rechteck. Wie im letzten Kapitel gelernt, können wir in dieses Image zeichnen, indem wir einen Fokus draufsetzen. Zu Demonstrationszwecken nehmen wir zunächst einmal ein einfaches Rot, welches eine leichte Transparenz hat. … // Pasteboard erstellen und befuellen NSPasteboard* pasteboard = [NSPasteboard pasteboardWithName:NSDragPboard]; NSArray* dragTypes = [NSArray arrayWithObject:NSTIFFPboardType]; [pasteboard declareTypes:dragTypes owner:nil]; [pasteboard setData:[dragImage TIFFRepresentation] forType:NSTIFFPboardType]; …
Überraschend ist vielleicht die Verwendung des Pasteboards. Letztlich ist unser Drag & Drop aber nichts anderes als ein Copy & Paste, wobei einfach ein besonderes Drag-Pasteboard verwendet wird. Das Pasteboard müssen wir freilich später noch befüllen, nämlich dann, wenn wir die Daten vom Controller abholen. Hier setzen wir einfach das Image als TIFF in das Pasteboard – zu Testzwecken. 188
Kapitel 2
Nutzeraktionen: Events und Responder
… // Ab dafuer [self dragImage:dragImage at:imageRect.origin offset:NSZeroSize event:event pasteboard:pasteboard source:self slideBack:YES]; }
Zuletzt wird das Drag & Drop mit dragImage:at:offset:event:pasteboard:source: slideBack: (NSView) gestartet. Der Offset-Parameter wird dabei nicht verwendet. Übergeben Sie einfach bei dem at-Parameter den Ursprung des Rechteckes im View. Mit slideBack wird bestimmt, ob bei einem gescheiterten Drag & Drop – so war es bisher – das Bild wieder automatisch an seinen Ursprungsort zurückwandern soll. Soll es. Bitte probieren Sie das Programm aus.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 36« von der Webseite herunterladen. Um das Ganze noch zu verschönern, fügen wir nur noch kurz den Code zum Zeichnen der Karten ein: - (void)mouseDragged:(NSEvent*)event { … // Groesse berechnen … NSRect imageRect = NSUnionRect( draggedRect, lastRect ); NSSize offset = NSMakeSize( imageRect.origin.x - 10.5, imageRect.origin.y - 10.5 ); imageRect.size.width += 21.0; imageRect.size.height += 21.0; // Bild zeichnen NSImage* dragImage = [[[NSImage alloc] initWithSize:imageRect.size] autorelease]; [dragImage lockFocus];
189
SmartBooks
Objective-C und Cocoa – Band 2
NSInteger suit; NSInteger value; NSInteger drawIndex; for( drawIndex = index; drawIndex < count; drawIndex++ ) { id card = [self.cards objectAtIndex:drawIndex]; NSRect drawRect = [self rectForCardWithIndex:drawIndex count:count getIsJammed:NULL]; drawRect = NSOffsetRect( drawRect, -offset.width, -offset.height ); [NSGraphicsContext saveGraphicsState]; NSShadow* shadow = [[[NSShadow alloc] init] autorelease]; [shadow setShadowBlurRadius:10.0]; [shadow set]; [self drawCardBorderInRect:drawRect selected:NO]; [NSGraphicsContext restoreGraphicsState];
suit = [[card valueForKey:@"suit"] integerValue]; value = [[card valueForKey:@"value"] integerValue]; [self drawCardFaceWithSuit:suit value:value inRect:drawRect]; } [dragImage unlockFocus]; // Pasteboard erstellen … // Ab dafuer imageRect.origin.x -= 10.5; imageRect.origin.y -= 10.5; [self dragImage:dragImage at:imageRect.origin offset:NSZeroSize event:event pasteboard:pasteboard source:self slideBack:YES]; }
190
Kapitel 2
Nutzeraktionen: Events und Responder
Nachdem Sie das gemacht haben, ziehen Sie bitte den gesamten Block mit den Events (ab -acceptsFirstMouse) und -initWithFrame: sowie -dealloc an das Ende der Datei. Beachten Sie bitte auch, dass wegen der Umrandung und des Schattens ein Offset für das Zeichnen der Karten im Image notwendig ist. Schauen Sie sich an, wie mit diesem umzugehen ist.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 37« von der Webseite herunterladen. Sie können dies schon einmal testen. Aber etwas ist noch zu erledigen: Wir können jetzt auch die gezogenen Karten vom Stapel entfernen. Allerdings soll dies nicht tatsächlich geschehen, sondern nur in der Darstellung. Wir befinden uns ja im View. Der sollte nicht voreilig an den Daten herumfummeln, bevor er den Controller befragt hat. Das Interface dazu programmieren wir aber erst später. Dazu führen wir eine neue Eigenschaft ein, die wir beim Zeichnen berücksichtigen, im Header: @interface CardStackView : NSView { … NSInteger draggedCardsCount; NSRect dragRect; } @property( copy ) NSArray* cards; @property NSRectEdge alignment; @property NSInteger draggedCardsCount;
und in der Implementierung: @implementation CardStackView @synthesize alignment, draggedCardsCount, dragRect;
Diese setzen wir im Dragging: - (void)mouseDragged:(NSEvent*)event … // Gezogene Karten ausblenden self.draggedCardsCount = [self.cards count] - index; [self setNeedsDisplay:YES];
191
SmartBooks
Objective-C und Cocoa – Band 2
// Ab dafuer … // Gezogene Karten wieder einfuegen self.draggedCardsCount = 0; [self setNeedsDisplay:YES]; }
Beachten Sie, dass dies funktioniert, weil die Methode -dragImage:at:offset:event: pasteboard:source:slideBack: eine modale Implementierung des Draggings enthält und daher erst nach Abschluss des Drag-Zyklusses zurückkehrt. Bei den letzten Anweisungen ist daher der Drag-Zyklus bereits beendet. In -drawRect: zeichnen wir einfach die entsprechenden Karten nicht mehr: - (void)drawRect:(NSRect)rect { … for( index = 0; index < (count - self.draggedCardsCount); index++ ) { … } }
Sie sollten jetzt Karten (virtuell) ziehen können.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 38« von der Webseite herunterladen.
Dragging-Source Die View-Instanz, von der die Karten gezogen werden, ist jetzt durch den Parameter source in dragImage:at:offset:event:pasteboard:source:slideBack; zu einer sogenannten Dragging-Quelle (Dragging-Source) geworden. Dies bedeutet, dass sie verschiedene Nachrichten während des Draggings erhält. Letztlich ist das ein Fall der Delegations. Da diese Methoden bereits als Kategorie von NSObject implementiert sind, müssen sie eigentlich gar nicht von uns geschrieben werden. Allerdings ist dann das Dragging funktionslos. Und wir wollen uns ohnehin schon nur zu Lehrzwecken die einzelnen Methoden anschauen.
192
Kapitel 2
Nutzeraktionen: Events und Responder
GRUNDLAGEN Natürlich kann man daran denken, gleich einen Controller zur DraggingSource zu machen. Wir wollen das hier aber wie bei einem Tableview implementieren, der ebenfalls die betreffenden Nachrichten empfängt und dann an sein Delegate vorgekaut weiterleitet. Vor der Initialisierung fügen Sie bitte ein: - ( NSDragOperation) draggingSourceOperationMaskForLocal:(BOOL)local { NSLog( @"Operation fuer local %d", local ); if( local ) { return NSDragOperationMove | NSDragOperationCopy | NSDragOperationDelete; } else { return NSDragOperationCopy; } } …
Diese Methode, die den Grundstock einer Dragging-Source bildet, gibt zurück, welche Drag-Operationen überhaupt erlaubt sein sollen. Als Konstanten existieren: Konstante
Bedeutung
NSDragOperationNone
Keine Operation
NSDragOperationMove
Elemente werden ins Ziel verschoben.
NSDragOperationCopy
Elemente werden ins Ziel kopiert.
NSDragOperationLink
Im Ziel wird ein Verweis auf die Elemente erzeugt.
NSDragOperationDelete
Die Elemente werden gelöscht.
NSDragOperationEvery
Jede Operation ist zulässig.
NSDragOperationGeneric Das Ziel bestimmt die Operation. NSDragOperationPrivate
Die Operation wird zwischen Quelle und Ziel selbst ausgehandelt.
193
SmartBooks
Objective-C und Cocoa – Band 2
Aus den Konstanten lassen sich Bitpattern zaubern. Hierbei können Sie mittels des Operators | (Bit-Oder) die Konstanten kombinieren. In unserem Beispiel sagen wir also, dass eine Move-, Delete- oder eine Copy-Operation vorgenommen werden soll.
GRUNDLAGEN Das funktioniert freilich nicht mit allen Konstanten von Cocoa, sondern nur dann, wenn diese auf bestimmte Weise definiert sind. Die technischen Hintergründe erfahren Sie in der Referenz unter »Bit-Operationen«. Freilich ergibt dies bei unserer Applikationen keinen rechten Sinn, weshalb wir das später zu einem ausschließlichen Move ändern werden. Aber so erhalten wir die Chance, die nächste Methode des Dragging-Source-Protokolles – und weitere – zu erläutern: … - (BOOL)ignoreModifierKeysWhileDragging { return NO; } …
Hiermit bestimmen Sie, ob die Modifier-Tasten (Umschalttaste usw.) erlaubt sind. Wir erlauben sie einmal zu Testzwecken. Vom System wird folgende Zuordnung vorgenommen, falls dies erlaubt ist: Modifier-Key
Operation
[ctrl]
Link
[alt] (Optionstaste)
Copy
[cmd] (Befehstaste)
Generic
Weitere nützliche Methoden sind: … - (void)draggedImage:(NSImage*)image beganAt:(NSPoint)location { NSLog( @"Drag-Zyklus begonnen" ); } …
194
Kapitel 2
Nutzeraktionen: Events und Responder
Diese Methode wird unmittelbar nach dem Start des Drag-&-Drop-Zyklusses ausgeführt. Der location-Parameter bezieht sich übrigens auf die Kante des Images, nicht auf die Mausposition. Sie erfolgt im Koordinatensystem des Bildschirmes, da wir ja die Drag-Operation auch außerhalb des Fensters beenden können. Mit den Methoden -convertPointFromBase: (NSWindow) und -convertPoint:fromView: (NSView) können Sie das aber in das Koordinatensystem des Views umrechnen. … - (void)draggedImage:(NSImage*)image endedAt:(NSPoint)location operation:(NSDragOperation)operation { if( operation == NSDragOperationDelete ) { NSLog( @"Karte loeschen" ); } }
Mit der Implementierung dieser Methode erreichen wir, dass der Dragging-SourceView darüber informiert wird, dass die Drag-Operation beendet, also letztlich die Maustaste losgelassen wurde. Bedenken Sie hierbei bitte, dass dies in einem anderen View geschehen sein kann. Wichtig ist hier der Parameter operation, der angibt, welche Operation letztlich ausgeführt werden soll. Dies hängt ja auch vom Ziel ab. Wir vergleichen hier einfach mal mit der Operation für das Löschen. Übersetzen Sie den Code und führen Sie eine Drag-Operation aus. Sie sollten im Log sehen, dass der Zyklus gestartet und beendet wird. Jetzt ziehen Sie den Stapel auf den Desktop. Dort erscheint eine Datei – in Wahrheit spiegelt das der Finder nur vor –, die mit einem Doppelklick geöffnet werden kann. Siehe da: Es erscheint unsere Zeichnung. Ebenso können Sie das Bild auf die Vorschau-Applikation im Dock fallen lassen: Et voìlá! Nun ziehen Sie die Karten auf den Papierkorb im Dock. Sie sehen im Log, dass jetzt bei unserer Methode -draggedImage:endedAt:operation: Löschen als Operation ankommt und daher der entsprechende Text geloggt wird.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 39« von der Webseite herunterladen.
195
SmartBooks
Objective-C und Cocoa – Band 2
Löschen Sie bitte wieder die Methoden -draggedImage:beganAt: und -draged Image:endedAt:operation. Weitere Nachrichten, die eine Dragging-Quelle auf Wunsch erhält, sind -dragged Image:movedTo: und nameOfPromisedFileDroppedAtDestination:. Letztere wird aufgerufen, wenn wir im Pasteboard einen entsprechenden Eintrag hinterlassen haben, dass eine Datei erst noch erzeugt werden soll.
Dragging-Destination Unsere Kartenstapel sind aber nicht nur Quellen für einen Drag-Zyklus, sondern auch deren Senken (Dragging-Destination). Entsprechend existiert ein Protokoll für Nachrichten, die an eine Senke gesendet werden. Die Dokumentation unterscheidet dabei zwei Gruppen: diejenigen Methoden, die während eines Drag-Zyklusses gesendet werden, um etwa dem Nutzer graphisches Feedback zu geben, und diejenigen, die dann die eigentliche Drag-Operation ausführen. Registrierung Bevor wir aber loslegen, müssen wir zunächst bekanntgeben, welche Arten von Daten wir überhaupt behandeln wollen. CardStackView.m: - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { … // Drop-Types registrieren NSArray* dropTypes = [NSArray arrayWithObject:NSTIFFPboardType]; [self registerForDraggedTypes:dropTypes]; } return self; }
Visualisierung Schauen wir auf die Methoden, die uns während eines Drag-Zyklusses Informationen über den Zustand geben. Diese fügen Sie einfach nach den bisherigen Dragging-Source-Methoden ein. Wir beginnen aber mit einer Hilfsmethode: - (NSString*)operationText:(NSDragOperation)operation { NSString* outText = @""; if( operation & NSDragOperationCopy ) { outText = [outText stringByAppendingString:@"Copy " ];
196
Kapitel 2
Nutzeraktionen: Events und Responder
} if( operation & NSDragOperationMove ) { outText = [outText stringByAppendingString:@"Move "]; } if( operation & NSDragOperationDelete ) { outText = [outText stringByAppendingString:@"Delete "]; } return outText; } …
Hier sehen Sie, wie aus einer Kombination von Operationen – Sie erinnern sich an den Bit-Oder-Operator – mittels des Bit-Und-Operators & wieder aufgedröselt werden kann: Einfach mit der gewünschten Testkonstante kombinieren. Der Hintergrund ist, dass diese Methoden zunächst eine Kombination der gewünschten Operationen erhalten können. Das testen wir auch gleich im Einzelnen. … - (NSDragOperation)draggingEntered:(id)info { NSDragOperation op = [info draggingSourceOperationMask]; NSLog( @"Entered %@", [self operationText:op] ); if( [self.cards count] > 0 ) { self.selectedCardsIndices = [NSIndexSet indexSetWithIndex:[self.cards count]-1]; [self setNeedsDisplay:YES]; } if( op & NSDragOperationMove ) { return NSDragOperationMove; } else if( op & NSDragOperationCopy ) { return NSDragOperationCopy; } else { return NSDragOperationNone; } } - (void)draggingExited:(id)info { self.selectedCardsIndices = [NSIndexSet indexSet];
197
SmartBooks
Objective-C und Cocoa – Band 2
[self setNeedsDisplay:YES]; NSLog( @"exited constants %X", [info draggingSourceOperationMask] ); } …
Diese Methoden werden aufgerufen, wenn die Maus eines unserer Views betritt bzw. wieder verlässt. Man kann das dazu nutzen, um visuelles Feedback zu liefern, wie wir das mit der Selektion machen. Wie bei allen folgenden Methoden wird eine Information mitgeliefert, die auf das NSDraggingInfo-Protokoll hört, also die dort definierten Methoden anbietet. Wir geben hier einfach die Operationen aus. Beachten Sie aber bitte, dass -draggingEntered einen Rückgabewert hat, der angibt, welche Operation ausgeführt werden soll. Wir erhalten also einen Satz von Operationen (nämlich diejenigen, die uns die Quelle anbot) und filtern uns daraus diejenige, die uns genehm ist. Wir schauen also nach, ob ein Move angeboten wird, und nehmen dieses, falls dem so ist. Andernfalls schauen wir nach, ob wenigstens ein Copy angeboten wird, und geben uns damit zufrieden. Ist dies auch nicht der Fall, sagen wir, dass wir mit der ganzen Sache nichts zu tun haben wollen (NSDragOperationNone). … - (void)draggingEnded:(id)info { self.selectedCardsIndices = [NSIndexSet indexSet]; NSDragOperation op = [info draggingSourceOperationMask]; NSLog( @"Ended %@", [self operationText:op] ); } - (BOOL)wantsPeriodicDraggingUpdates
{ return NO; }
- (NSDragOperation)draggingUpdated:(id)info { NSDragOperation op = [info draggingSourceOperationMask]; NSLog( @"Updated %@", [self operationText:op] ); if( op & NSDragOperationMove ) { return NSDragOperationMove; } else if( op & NSDragOperationCopy ) { return NSDragOperationCopy; } else { return NSDragOperationNone; } }
198
Kapitel 2
Nutzeraktionen: Events und Responder
Während die erste Methode wohl kaum der Erläuterung bedarf (es wird lediglich die unsichtbare Selektion gelöscht), verhält es sich bei den beiden letzten anders: Mit -wantsPeriodicDraggingUpdates können wir mitteilen, ob wir ständig UpdateNachrichten erhalten wollen (YES) oder nur dann, wenn sich etwas geändert hat (NO). Ausnahmsweise kann ein solches Sperrfeuer hilfreich sein für visuelle Effekte – hier nicht. Womit wir bei der letzten Methode wären, die zwischendurch aufgerufen wird, wenn eine Änderung vorliegt. Dies ist der Fall, wenn die Maus auf dem View bewegt wurde oder wenn sich die Modifier geändert haben. Starten Sie bitte das Programm. Sie werden im Log die einzelnen Operationen sehen, wenn Sie Karten zunächst im Ausgangsview ziehen und dann in ein weiteres View wandern: >… >… >… … >… >… >… >… … >… >… >… >…
Drag-Zyklus begonnen Entered Copy Move Delete Updated Copy Move Delete Updated Copy Move Delete Exited Copy Move Delete Entered Copy Move Delete Updated Copy Move Delete Updated Copy Move Delete Ended Copy Move Delete Ended Copy Move Delete Ended Copy Move Delete
Vielleicht wundert Sie zunächst, dass die Methode -draggingEnded: mehrfach aufgerufen wird. Sie wird genaugenommen für jedes einmal betretene View (also solche, die auch die vorangegangenen Nachrichten bekamen) ausgeführt. Dies dient dazu, etwaig zwischenzeitlich erstellte Arbeiten für die Visualisierung des DragVorganges wieder aufzuräumen. Sie dient nicht dazu, die Operation auszuführen. Dafür haben wir ja einen eigenen Satz an Methoden, den wir noch besprechen. Auch Dragging-Updated liefert ja einen Rückgabewert für die gewünschte Operation. Und dies testen wir auch einmal. Starten Sie eine Drag-Operation und drücken Sie dann auf die Optionstaste. Sie sehen jetzt, dass sich der Mauszeiger verändert und das grüne Pluszeichen erscheint. Wenn Sie in den Log schauen, sehen Sie die Ursache: Das System bietet jetzt nur noch Copy als Operation an, so dass unsere Abfrage im -draggingUpdated: als Returnwert NSDragOperationCopy liefert. 199
SmartBooks
Objective-C und Cocoa – Band 2
>… Entered Copy Move Delete >… Updated Copy Move Delete >… Updated Copy
Dies veranlasst das System, den Cursor zu ändern. Lassen Sie die Taste wieder los, erscheinen wieder die alten Operationen. Nun drücken Sie bitte die Controltaste. Die entsprechende Operation wäre Link. Da unsere Quelle die jedoch nicht anbot, erhalten wir im -draggingUpdated: keine Operation zur Auswahl. Entsprechend wird von uns NSDragOperationNone geliefert: >… Entered Copy Move Delete >… Updated Copy Move Delete >… Updated Fenster
Anwendungsebene View
View
Operations-
Gefiltertes Angebot
angebot
Operation
Systemebene Modifier
Die Anwendung(en) und das System ermitteln gemeinsam die Operation.
Also: Das System ermittelt aus den Angaben der Quelle und dem aktuellen Zustand der Modifier, welche Operation ausgeführt werden kann. Wenn mehrere übrig bleiben, muss die Senke eine Auswahl treffen.
200
Kapitel 2
Nutzeraktionen: Events und Responder
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 40« von der Webseite herunterladen. Ausführen Kommen wir zur zweiten Gruppe: den Methoden, die das Drag & Drop wirklich durchführen. So richtig können wir das ja noch nicht implementieren. Dennoch müssen Sie bereits die Regeln für die Kommunikation zwischen Drag-Server und Views beherrschen. Folgende Methoden fügen Sie bitten nach den soeben Eingefügten hinzu: - (BOOL)prepareForDragOperation:(id)info { NSDragOperation op = [info draggingSourceOperationMask]; NSLog( @"prepare %@", [self operationText:op] ); return YES; } …
Die optionale Methode wird ausgeführt, nachdem die Maustaste losgelassen wurde und bevor das Bild vom Bildschirm veschwindet. Dies ist jedoch nur dann der Fall, wenn der letzte Aufruf von -draggingUpdated: oder – falls -draggingUpdated: nicht implementiert wurde – -draggingEntered: eine gültige Operation zurücklieferte. Die Methode muss YES liefern, wenn der Drag-&-Drop-Zyklus fortgeführt werden soll. … - (BOOL)performDragOperation:(id)info { NSDragOperation op = [info draggingSourceOperationMask]; NSLog( @"perform %@", [self operationText:op] ); if((op & NSDragOperationMove) || (op & NSDragOperationCopy)) { return YES; } else { return NO; } } …
Hier gilt Entsprechendes, wenn -prepareForDragOperation: nicht implementiert wurde. Ist es implementiert, so hängt der Aufruf der Methode davon ab, ob vorher YES geliefert wurde. In dem Code-Beispiel wird in Abhängigkeit der angebotenen 201
SmartBooks
Objective-C und Cocoa – Band 2
Drag-Operationen die Operation erlaubt oder verboten. Natürlich müsste im ifZweig dann der eigentliche Code stehen – machen wir auch noch. … - (void)concludeDragOperation:(id)info { NSDragOperation op = [info draggingSourceOperationMask]; NSLog( @"Concluded %@", [self operationText:op] ); }
Diese Methode wird aufgerufen, nachdem eine Drag-Operation beendet wurde. Sie können hier also dort hinterlassenen Restmüll entsorgen. Bitte spielen Sie etwas mit der Applikation herum und schauen sich das jeweils im Log an. Beachten Sie, dass bei einer zulässigen Drag-Operation das Image nicht zum Ursprung zurückgeschoben wird, sehr wohl aber bei einer unzulässigen. Da wir noch nichts Funktionales machen, ist das auf den ersten Blick nicht einfach zu unterscheiden. Probieren Sie einmal einen Drop bei gehaltener Controltaste aus. Sie bemerken dann den Unterschied.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 41« von der Webseite herunterladen.
Cursor-Rects Häufig kann es sinnvoll sein, die Maus zu beobachten. Häufigster Fall dafür ist, dass in bestimmten Bereichen der Mauszeiger verändert sein soll. Hierfür bietet Cocoa eine bequeme Methode an: In einer Methode -resetCursorRects: können Sie für Bereiche Mauszeiger setzen, die dann automatisch vom System gezeichnet werden. Fügen Sie nach -mouseDragged: folgenden Code ein: - (void)resetCursorRects { NSUInteger index = [self.cards count]; if( [self.cards count] > 0 ) { NSRect lastCardRect = [self rectForCardWithIndex:index-1 count:index getIsJammed:NULL]; NSCursor* cursor = [NSCursor openHandCursor]; [self addCursorRect:lastCardRect cursor:cursor]; } }
202
Kapitel 2
Nutzeraktionen: Events und Responder
Natürlich kann es vorkommen, dass diese Bereiche neu kalkuliert werden müssen. Dies ist etwa bei uns der Fall, wenn sich der Kartenstapel verändert. Dies muss dann dem Fenster mitgeteilt werden, damit dieses wiederum bei den Views die Bereiche abfragt. Bauen wir auch das ein: - (void)setCards:(NSArray*)value { … // Mausrechtecke neu berechnen [[self window] invalidateCursorRectsForView:self]; // Standardaccessor … }
Übersetzen, starten, testen: Wenn Sie jetzt das Rechteck der untersten Karte betreten, ändert sich automatisch der Mauszeiger.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 42« von der Webseite herunterladen. Entfernen Sie bitte -resetCursorRects wieder.
Tracking-Areas Eine flexiblere Möglichkeit bieten die Tracking-Areas. Damit können Sie beliebige Bereiche Ihrer Views überwachen. Sie existieren allerdings erst seit OS X 10.5 und ersetzen die früheren, doch etwas hölzernen Tracking-Rects. Um mal etwas auszuprobieren, machen wir zunächst eine private Methode bekannt: @interface CardStackView( PrivateMethodsAddition ) … - (void)updateTrackingAreas; @end
Im Setter rufen wir diese Methode auf, um die Bereiche anzupassen: - (void)setCards:(NSArray*)value { …
203
SmartBooks
Objective-C und Cocoa – Band 2
// Trackingbereiche neu berechnen [self updateTrackingAreas]; // Standardaccessor … }
Schließlich implementieren wir die eigentliche Methode anstelle von -resetCursorRects: -(void)updateTrackingAreas { NSUInteger index; NSTrackingArea* area; // Alte Bereiche entfernen index = [[self trackingAreas] count]; if( index > 0 ) { area = [[self trackingAreas] objectAtIndex:0]; [self removeTrackingArea:area]; } // Neuen einfuegen index = [self.cards count]; if( index > 0 ) { NSRect rect = [self rectForCardWithIndex:index-1 count:index getIsJammed:NULL]; // Welche Events sollen ausgeloest werden NSTrackingAreaOptions eventOptions; eventOptions = NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved; // Unter welchen Umstaenden NSTrackingAreaOptions activationOptions; activationOptions = NSTrackingActiveInActiveApp; NSTrackingAreaOptions options; // Area erzeugen options = eventOptions | activationOptions; area = [[[NSTrackingArea alloc]
204
Kapitel 2
Nutzeraktionen: Events und Responder
initWithRect:rect options:options owner:self userInfo:nil] autorelease]; // Und hinzufuegen [self addTrackingArea:area]; } }
Erläuterungsbedürftig sind hier die Optionen. Es existieren drei Sätze:
•
Eine Gruppe von Optionen bestimmt, welche Ereignisse ausgelöst werden sollen. Hier wird verlangt, dass jedes Betreten und Verlassen des Bereiches und jede Bewegung im Bereich beobachtet werden soll. Mindestens eine Option muss in dieser Gruppe gesetzt sein.
•
Mit einer weiteren Gruppe von Optionen kann bestimmt werden, unter welchen Umständen die Ereignisse ausgelöst werden sollen. Wir sagen hier, dass dies nur der Fall sein soll, wenn die Applikation aktiv ist. Genau eine Option muss in dieser Gruppe gesetzt sein.
•
Weitere Optionen, die wir nicht benutzen, können verwendet werden. So lassen sich etwa Tracking-Areas auch bei einem Drag-&-Drop-Zyklus beobachten (was nicht Standard ist).
Bitte schlagen Sie die Optionen in der Dokumentation nach. Diese Tracking-Areas erzeugen uns jetzt entsprechende Nachrichten. Dazu müssen wir freilich die passenden Methoden implementieren, und zwar nach -updateTrackingAreas: - (void)mouseEntered:(NSEvent*)event { NSLog( @"Reinspiel" ); } - (void)mouseExited:(NSEvent*)event { NSLog( @"Rausspiel" ); }
205
SmartBooks
Objective-C und Cocoa – Band 2
- (void)mouseMoved:(NSEvent*)event { NSLog( @"Bewegungsspiel" ); }
Bitte testen Sie das und beobachten Sie dabei den Log.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 43« von der Webseite herunterladen. Bitte entfernen Sie wieder die Methoden mit Ausnahme von -mouseMoved:, den Aufruf in -setCards: und die Forward-Declaration in der Kategorie.
Mouse-Move-Event Sie können auch Bewegungen der Maus generell beobachten. Da dies jedoch kritisch für die Performance des Systems ist, müssen einige Bedingungen erfüllt sein. Zunächst müssen wir unser Event-Window dafür anmelden. Hierzu fügen wir dort die Methode -awakeFromNib in EventWindow.m ein: - (void)awakeFromNib { [self setAcceptsMouseMovedEvents:YES]; } @end
Hiermit teilen wir dem System mit, dass wir überhaupt Mouse-Moved-Events erhalten wollen. Als Nächstes muss eine Eigenschaft vorliegen, die uns auch gleich in den nächsten Abschnitt gleiten lassen wird. Das View muss der First-Responder sein, Sie wissen schon, jenes View, welches üblicherweise den Focus-Ring erhält (den wir noch nicht zeichnen.) Nun ist es jedoch nicht so, dass ein View das automatisch wird, sondern nur dann, wenn es dies akzeptiert: - (BOOL)acceptsFirstResponder { return YES; } - (void)mouseMoved:(NSEvent*)event { NSLog( @"Bewegungsspiel %p", self ); }
206
Kapitel 2
Nutzeraktionen: Events und Responder
Wie Sie leicht erkennen können, muss dazu die Methode -acceptsFirstResponder überschrieben werden und YES zurückliefern. Da es immer nur einen First-Responder geben kann, wird auch nur ein View die Events erhalten. Um diese unterscheiden zu können, lassen wir uns zusätzlich die ID des Views ausgeben. Wenn Sie nun das Programm starten, werden Sie entsprechende Mitteilungen im Log sehen. Das Fenster hat automatisch den ersten Stapel zum First-Responder gemacht. Klicken Sie auf einen anderen Stapel, so wird dieser zum First-Responder. Sie können sehen, dass sich die ID im Log ändert: … >… >… >… >… >… >… …
Bewegungsspiel 0x135df0 Bewegungsspiel 0x135df0 in TableBaizeView in EventWindow Bewegungsspiel 0x136780 Bewegungsspiel 0x136780
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 44« von der Webseite herunterladen. Diese etwas antiquarische Art der Mausbeobachtung beseitigen wir jetzt wieder: -awakeFromNib in EventWindow.m löschen und -mouseMoved: in CardsStackView.m entfernen. -acceptsFirstResponder lassen Sie aber bitte drin, denn das benötigen wir jetzt noch:
Tastaturevents Eine Gruppe von Events behandelt Ereignisse, die von der Tastatur ausgelöst werden. Dies ist übrigens weniger bedeutend, als Sie vermuten. Zum einen werden für Tasteneingaben zumeist Standardklassen von Cocoa genutzt. Aber selbst wenn diese angepasst werden müssen, geht die Ableitung zum anderen nicht von NSView aus, sondern erfolgt im Textsystem, es sei denn, Sie haben Lust, das komplette Textsystem mit Font-Unterstützung, kombinierten Zeichen, Scrolling, Layout usw. nachzuprogrammieren. – Nein, dazu haben Sie keine Lust. Da das Textsystem in einem gesonderten Kapitel besprochen wird, verbleiben hier nur noch Fälle, dass wir Tasten zu etwas anderes benutzen. Wir werden dies machen, um Stapel und Karten zu selektieren. 207
SmartBooks
Objective-C und Cocoa – Band 2
Eventdispatch Der Verlauf eines Tasturevents ist etwas komplizierter, weil Tasten in unterschiedlicher Weise interpretiert werden können.
•
Zuallererst werden diejenigen Tasten aussortiert, die systemweite Bedeutung haben. Das ist etwa [Befehl]+[Tab] zum Wechseln der Applikation oder [Befehl]+[Leer] zum Öffnen von Spotlight. Von diesen Tasten erfährt unsere Applikation erst gar nichts.
POWER Sie können diese sogenannten Hot-Keys auch selbst registrieren. Und freuen Sie sich: Wir werden das an späterer Stelle sogar einmal machen. Hier bleiben wir aber bitte bei der Event-Abarbeitung auf der Viewschicht.
•
Dann wird nachgeschaut, ob wir ein Tastaturkürzel für den Tastendruck haben. Dies wird dann ausgehend vom Fenster die Viewhierarchie von oben nach unten (!) mit -performKeyEquivalent: zu verteilen versuchen. Sie können ja im Interface Builder für Menüs und Buttons Tastaturkürzeln eingeben (Band I, S. 382 f.)
•
Laut der Dokumentation werden sodann die Tasten ausgesondert, die das aktuelle View auswählen (Key-Loop, Band I, S. 382) also insbesondere [Tab]. Wir haben das getestet und können nur sagen, dass dies falsch ist: In Wahrheit wird zunächst die Methode -keyDown: des aktuellen (First-Responder) Views aufgerufen. Schluckt dieses das Event, so erfolgt keine Änderung des First-Responders. Also, nach unseren Versuchen: Als Nächstes wird versucht, -keyDown: des First-Responders auszuführen.
•
Leitet das View das Event die Responder-Chain hoch und landet es schließlich bei NSWindow, so werden die Tasten für die Key-Loop ausgewertet. Ebenfalls erfolgt hier eine Auswertung von Spezialtasten, die fest einer Methode zugeordnet sind, wie Page-Down. Das System erzeugt dann eine entsprechende Nachricht (hier: pageDown:), die gesondert gefangen werden kann.
208
Kapitel 2
Nutzeraktionen: Events und Responder
Eventmethoden Es existieren drei Methoden in drei Gruppen:
•
Das bereits angesprochene -performKeyEquivalent für Tastaturkürzel. Da allerdings NSButton bereits eine Implementierung enthält, lohnt sich das Überschreiben in der Regel nicht.
• •
-keyDown: und -keyUp: teilen einen Tastendruck auf eine »echte« Taste mit. -flagsChanged: wird ausgelöst, wenn eine der Umschalttasten (Modifier) gedrückt wurde.
Tastendruck Wir wollen zunächst den Tastendruck abfangen, um Karten im Stapel zu selektieren. Damit wir überhaupt Tastendrücke erhalten, müssen wir akzeptieren, FirstResponder zu sein. Das hatten wir im letzten Abschnitt mit -acceptsFirstResponder gemacht. Dazu gesellen wir drei weitere Methoden: - (BOOL)acceptsFirstResponder { return YES; } - (BOOL)becomeFirstResponder { if( [self.cards count] > 0 ) { self.selectedCardsIndices = [NSIndexSet indexSetWithIndex:[self.cards count]-1]; } return YES; } - (BOOL)resignFirstResponder { self.selectedCardsIndices = [NSIndexSet indexSet]; return YES; } - (void)keyDown:(NSEvent*)event { NSLog( @"key code: %d", [event keyCode] ); NSString* characters = [event characters]; unichar character = [characters characterAtIndex:0]; NSLog( @"character: %C", character ); }
209
SmartBooks
Objective-C und Cocoa – Band 2
Die beiden ersten der neuen Methoden werden aufgerufen, wenn unser View den Focus bekommt bzw. verliert. Man kann übrigens die Änderung des Focus verhindern, indem man NO zurückliefert. Die dritte Methode wird ausgeführt, wenn eine Taste gedrückt wurde. Hierbei erhalten wir zwei Codes, nämlich den Key-Code und den Character-Code. Ersterer wird nur bei Sondertasten (Funktionstasten usw.) geliefert und ist nicht gut durchdefiniert. Man sollte diese Methode nicht mehr verwenden. Da wir in Zeiten von Unicode leben, kann man mit characters sowohl die normalen Zeichen erreichen wie auch Sondertasten. Probieren Sie es einfach einmal aus. Außerdem existiert noch die Methode -charactersIgnoringModifiers (NSEvent), welche die Rohzeichen, also etwa ohne Berücksichtigung von [Option] ermitteln.
AUFGEPASST Kombinierte Zeichen wie [`]+[a] zu à können allerdings nur über den KeyCode behandelt werden, da das einzelne accent (der Akzent auf dem a) noch kein Zeichen darstellt. Diese Dinge gehören allerdings zum Textsystem und sollten diesem tunlichst überlassen werden. Wir wollen damit aber etwas Sinnvolles machen. Wenn [Pfeil hoch] gedrückt wird, soll die Selektion eine Karte nach oben rutschen, bei [Pfeil herunter] nach unten. Programmieren wir das: - (void)keyDown:(NSEvent*)event { NSUInteger count = [self.cards count]; // Keine Karten, keine Reaktion if( count == 0 ) { [[self nextResponder] keyDown:event]; return; } NSUInteger selection = [self.selectedCardsIndices firstIndex]; unichar character = [[event characters] characterAtIndex:0]; switch( character ) { case NSUpArrowFunctionKey: if( selection > 0 ) { self.selectedCardsIndices = [NSIndexSet indexSetWithIndex:selection-1]; } break;
210
Kapitel 2
Nutzeraktionen: Events und Responder
case NSDownArrowFunctionKey: if( selection < (count-1) ) { self.selectedCardsIndices = [NSIndexSet indexSetWithIndex:selection+1]; } break; } [super keyDown:event]; }
Wenn Sie das Programm starten und testen, sehen Sie schon, dass mit den Pfeiltasten einzelne Karten selektiert werden können. Mit der Tabulatortaste landen Sie auf dem nächsten Stapel. Allerdings wird ständig der Beep-Ton ausgegeben. Der Grund dafür ist, dass wir die Taste mit -keyDown: (super) weiterreichen. In der Basisimplementierung leitet diese Methode nämlich das Event an den nächsten Responder weiter. Am Ende der Responder-Chain führt ein Key-Down jedoch zum Beep, da niemand die Taste abgearbeitet hat. Um dieses Verhalten zu vermeiden, können wir aber nicht ganz auf die Weiterleitung verzichten. Denn zum einen könnte es sich ja um die Tabulatortaste handeln. Die muss aber vom Fenster weiter hinten in der Responder-Chain bearbeitet werden können. Zum anderen existieren Fälle, in denen es sich zwar um eine unserer Tasten handelt, die Operation jedoch nicht ausgeführt werden kann, weil sich die Selektion bereits am Anschlag befindet. Langer Rede kurzer Sinn: Wir müssen genau dann das Event durch die Responder-Chain schicken, wenn wir es nicht behandelt haben: - (void)keyDown:(NSEvent*)event { … switch( character ) { case NSUpArrowFunctionKey: if( selection > 0 ) { self.selectedCardsIndices = [NSIndexSet indexSetWithIndex:selection-1]; // Richtige Taste, aber bereits am Anschlag } else { [super keyDown:event]; } break;
211
SmartBooks
Objective-C und Cocoa – Band 2
case NSDownArrowFunctionKey: if( selection < (count-1) ) { self.selectedCardsIndices = [NSIndexSet indexSetWithIndex:selection+1]; return; // Richtige Taste, aber bereits am Anschlag } else { [super keyDown:event]; } break; // Nicht benutzte Taste default: [super keyDown:event]; break; } // Keine Weiterleitung mehr! }
Jetzt klappt es auch mit dem Dong.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 45« von der Webseite herunterladen.
Modifier Wie bereits erwähnt, können auch Modifier abgefragt werden, und zwar bevor eine kombinierte Taste gedrückt wird. Dies kann etwa sinnvoll sein, wenn man auf einen Tastendruck hin Texte umschalten oder sonstige graphische Rückkoppelungen erzielen will. Wir schauen uns einfach mal den Mechanismus an. In CardStackView unterhalb der Methode -keyDown: bitte folgenden Code hinzufügen: - (void)flagsChanged:(NSEvent*)event { NSUInteger modifier = [event modifierFlags]; NSLog( @"%d", modifier ); modifier &= NSDeviceIndependentModifierFlagsMask; NSLog( @"%d", modifier ); if( modifier == 0 ) { NSLog( @"Kein Modifier" ); } else { NSString* outText = @"Modifier: ";
212
Kapitel 2
Nutzeraktionen: Events und Responder
if( modifier & NSShiftKeyMask ) { outText = [outText stringByAppendingString:@"Shift "]; } if( modifier & NSAlphaShiftKeyMask ) { outText = [outText stringByAppendingString:@"Feststell "]; } if( modifier & NSAlternateKeyMask ) { outText = [outText stringByAppendingString:@"Option "]; } if( modifier & NSControlKeyMask ) { outText = [outText stringByAppendingString:@"Control "]; } if( modifier & NSCommandKeyMask ) { outText = [outText stringByAppendingString:@"Befehl "]; } NSLog( @"%@", outText ); } }
Im unteren Teil der Methode sehen Sie wieder, dass sich einzelne Tasten aus der Bitmaske mit & herausfischen lassen. Dies sollten Sie langsam beherrschen. Der obere Teil ist jedoch etwas merkwürdig: Bei einem Modifier können mehrere Optionen gesetzt sein. Dabei unterscheidet man zwischen den geräteabhängigen und den geräteunabhängigen Optionen. Sie können das ausprobieren, indem Sie das Programm übersetzen und zunächst die linke Shifttaste drücken und wieder loslassen. Es erscheint: >… >… >… >… >… >…
131330 131072 Modifier: Shift 256 0 Kein Modifier
Machen Sie nun dasselbe mit der rechten Shifttaste: >… >… >… >… >… >…
131332 131072 Modifier: Shift 256 0 Kein Modifier
213
SmartBooks
Objective-C und Cocoa – Band 2
Sie sehen, dass sich der jeweils erste Wert verändert hat, der zweite nicht. Das liegt daran, dass die geräteabhängigen Codes zwischen rechter und linker Maustaste unterscheiden, die geräteunabhängigen nicht. Da wir jedoch mit modifier &= NSDeviceIndependentModifierFlagsMask;
vor der Ausgabe der zweiten Zeile die geräteabhängigen Codes herausfiltern, erscheint in dem Log hiernach kein Unterschied mehr. Die später in der Methode verwendeten Konstanten (NSShiftKeyMask usw.) sind sämtlich geräteunabhängig. Und Sie sollten auch nur diese verwenden! Wenn Ihre Applikation zwischen rechter und linker Shifttaste unterscheidet, wäre das ein Fall für Windows. Wollen wir nicht. Also bitte immer nur die vorgefertigten Konstanten verwenden. Eine vollständige Auflistung erhalten Sie, indem Sie in der Dokumentation nach einer der hier verwendeten Konstanten suchen.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 46« von der Webseite herunterladen. Beachten Sie bitte weiter, dass die Ausgabe nur ein Mal erfolgt, nämlich für den First-Responder. Möchten Sie fensterweit etwas darstellen oder etwa eine Statuszeile umschalten, so müssen Sie -flagsChanged: auf einer späteren Stufe der Responder-Chain implementieren. Meist wird dies das Fenster sein, welches dann die Statuszeile entsprechend umschaltet. Dies bedingt natürlich, dass der in der Hierarchie tiefer liegende View das Event weiterleitet. Sonst wird es ja abgearbeitet und kommt nie beim Fenster an. Letztlich wird man hier häufig die saubere Kapselung zwischen Fenster und Views nicht immer halten können.
214
Kapitel 2
Nutzeraktionen: Events und Responder
Actions Etwas anders sieht die Sache bei Actions aus: Zum einen handelt es sich bei diesen nicht um wirkliche Events. Sie sind ja nicht unmittelbar durch die Benutzeraktion wie Mausklick oder Tastaturkürzel ausgelöst worden, sondern erst mittelbar vom System auf ein solches erzeugt worden. Dementsprechend haben diese Actionmethoden als Parameter auch nicht eine Instanz von NSEvent, sondern den Absender. Allerdings ist auch hier wie bei den Eventmethoden eine Vielzahl von Methoden vorprogrammiert, die bereits die Klasse NSResponder kennt. Wir hatten etwa im letzten Kapitel das -print:. Wenn Sie einmal in der Dokumentation zu NSResponder nachschauen, sehen Sie eine stattliche Liste unter der Überschrift Responding to Action Messages. Diese entspricht der Liste der Methoden des First-Responders in MainMenu.xib (und jedem anderen Nib). Sie hatten auch schon im ersten Band (S. 480 ff.) gelernt, dass man sich selbst Methoden definieren kann, die in der Responder-Chain auftauchen sollen.
TIPP Bei eigenen Actionmethoden ist es freilich häufig nicht notwendig, die Responder-Chain zu benutzen. Man kann ja vom Button oder Menüeintrag unmittelbar auf den entsprechenden Controller eine Verbindung ziehen. Wichtig wird das Ganze, wenn sich View und Controller in verschiedenen Nibs befinden (Band I, S. 480). Außerdem kann es ausnahmsweise so sein, dass wir die Vorzüge der Responder-Chain selbst nutzen wollen, also je nach Selektierung eines Views unterschiedliche Methoden an unterschiedlichen Stellen unseres Codes abarbeiten wollen. Dies ist jedoch eher selten der Fall.
Applikation als Verteiler Zunächst müssen Sie wissen, dass jede Action über die Applikation verteilt wird. Dies gilt sogar dann, wenn Sie im Interface Builder einen bestimmten Adressaten ausgewählt haben. Es wird dann die Methode -sendAction:to:from: (NSApplication) ausgeführt. Diese erhält das Ziel und schickt einfach die Action dorthin. Haben wir im Interface Builder bestimmt – oder aus dem Code – dem Auslöser gesagt, dass er die Nachricht an den First-Responder schicken soll, so bedeutet dies nichts anderes, als dass der to-Parameter nil ist. -sendAction:to:from: fängt dann an, einen geeigneten Responder zu suchen. Hierzu dient die Methode -targetFor Action:to:from: (NSApplication). 215
SmartBooks
Objective-C und Cocoa – Band 2
Control Menü-Item
-sendAction:to:from: Key-Window
View
View
…
View
View
…
View
WindowController WindowDelegate
Main-Window
View WindowController WindowDelegate
Application (self) AppDelegate DocumentController
Etwas komplizierter, jedoch geordnet stellt sich das Actiondispatch dar.
216
Kapitel 2
Nutzeraktionen: Events und Responder
Erster Versuch: Responder-Chain des Key-Windows Zunächst einmal geschieht nicht viel anderes als auch bei Tastaturevents: First-Responder Zunächst wird versucht, die Action beim First-Responder desjenigen Fensters zuzustellen, welches gerade die aktive Tastatureingabe bearbeitet. Bitte beachten Sie hierbei, dass ja eine Applikation mehrere Fenster mit jeweils einem aktuellen FirstResponder haben kann. Es geht jetzt wiederum um das Fenster mit dem FocusRing – ganz wie beim Tastendrücken. Und ebenfalls wie beim Tastendrücken wird dabei entlang der View-Hierarchie gewandert. Window-Controller, Window-Delegate und Document Zudem wird allerdings geschaut, ob der Window-Controller oder das Delegate des Fensters eine entsprechende Methode anbieten. Bei dokumentenbasierten Applikationen befragt NSApplication schließlich das hinter dem Fenster liegende Dokument, ob eine entsprechende Methode vorhanden ist.
Zweiter Versuch: Responder-Chain des Main-Windows Jetzt erfolgt allerdings etwas anderes. Sie kennen ja schon Panels (Band I, S. 349), diese Fenster, die bei Deaktivierung der Applikation verschwinden. Inspektoren etwa, wie der des Interface Builders oder seine Library. Erkennbar sind diese an der schmalen Titelleiste. Und bereits in Band I (S. 500) wurde zudem darauf hingewiesen, dass diese Fenster nie die Eigenschaft Main-Window einer Applikation werden. Nur »normale« Fenster machen sich beim Klicken zum Main-Window. Wenn also auf ein Panel geklickt wird, so besteht weiterhin ein First-Responder in dem Main-Window. Mit anderen Worten: Wenn Sie gleichzeitig ein »normales« Fenster und ein Panel offen haben, wobei das Panel aktiviert wurde, wird zunächst versucht, in der Responder-Chain usw. des Panels die Actionmethode auszuführen. Gelingt dies nicht, so erfolgt die Ausführung in der Responder-Chain usw. des Main-Windows. Sie können dies selbst leicht testen. Wechseln Sie in den Interface Builder und ziehen Sie ein Label in unser Fenster. Vergrößern Sie es etwas. Gleichzeitig sorgen Sie dafür, dass das Attributes-Pane des Inspectors geöffnet ist. Klicken Sie nun so in das Label, dass Sie es editieren können und bewegen Sie kurz den Cursor. Mit Edit | Select All können Sie nun wieder den gesamten Text im Label selektieren. Klicken Sie nun im Attributes-Pane auf das Textfeld neben Title und bewegen Sie erneut den Cursor: Wieder können Sie mit Edit | Select all den Text selektieren. Bis hierher war das klar: Wer gerade die Tastatur hat, bekommt auch die Action.
217
SmartBooks
Objective-C und Cocoa – Band 2
Jetzt klicken Sie aber bitte in der Menüzeile auf Layout | Size to Fit, während sich der Mauscursor noch im Inspector befindet. Siehe da: Das Textfeld im anderen Fenster verändert sich! Dies ist nach dem bisher Gesagtem klar: Zunächst wird in der Responder-Chain usw. des Inspectors nach der Methode gesucht. Da sich dort aber nun wirklich niemand findet, der etwas mit Size To Fit anfangen kann, versucht es NSApplication beim »normalen« Main-Window. Hier wird man fündig, da natürlich Labels sich in der Größe anpassen können.
Dritter Versuch: Applikation Immer noch nicht genug, denn Actions können auch die Applikation betreffen. Einfachster Fall: der Eintrag Quit im Applikationsmenü, der die Action terminate: sendet. Daher versucht NSApplication als Nächstes, bei sich selbst eine entsprechende Methode zu finden. Ist auch dies erfolglos, so ist folgend das ApplicationDelegate dran.
TIPP Hier kann ein Problem auftauchen, wenn man das Verhalten der Applikation auf eine Action ändern will. Ein Delegate steht in der Schlange nach der Applikation, kann also keinen Einfluss ausüben. Dennoch muss man nicht NSApplication ableiten. Vielmehr bietet es sich an, anstelle der Standardaction wie terminate: einfach eine eigene zu definieren wie myTerminate:. Da damit NSApplication nichts anfangen kann, landet diese Nachricht beim Delegate. Das Ganze kann man übrigens auch anwenden, wenn ein View in der Responder-Chain eine Actionmethode abgreift, die man selbst erst auf Fensterebene abhandeln möchte, oder das Fenster eine Actionmethode implementiert, die man im Delegate ändern will. Immer noch nicht fertig, denn es wird bei dokumentenbasierten Applikationen zuallerletzt versucht, die Methode beim Document-Controller auszuführen. Wie bereits im ersten Band ausgeführt, sollten Sie daran nichts ändern. Problemfälle lassen sich durch eigene Actions lösen. Sie sollten aber den Gang der Actionmethode kennen, damit Sie sich nicht wundern, wenn mal bei Ihrer Instanz einfach nichts ankommt.
218
Kapitel 2
Nutzeraktionen: Events und Responder
Zusammenfassung Sie haben jetzt die Tiefen und Untiefen des Event-Handlings kennen gelernt. Es existieren noch mehr Methoden, etwa für Graphiktabletts. Wichtig ist das System von Responder-Chain und Abarbeitung mittels spezieller, von NSWindow hergestellten Eventmethoden. Sie sollten verstanden haben, wie das System jeweils den Adressaten einer Eventmethode sucht und wie gegebenenfalls Events weitergeleitet werden. Die Implementierung der einzelnen Eventmethoden sollte Ihnen keine Schwierigkeiten mehr bereiten. Sie müssen halt im Wesentlichen nachschlagen, welche Methoden vorhanden sind. Anlaufstelle ist die Dokumentation zu NSResponder. Das Dragging, welches wir hier gleich nebenbei mitprogrammiert haben, kann auf zweierlei Weise implementiert werden. Das ist hier als Anwendung zunächst einmal ausreichend. Sie sollten den Unterschied zwischen unmodaler und modaler Implementierung jedoch im Hinterkopf behalten. Im Kapitel über Nebenläufigkeit werden wir das nämlich noch einmal benötigen. Das Dispatching von Actionmethoden wurde auch gleich erläutert, weil es gut zur Thematik passt. Sie sollten aber weiterhin wie im ersten Band die jeweiligen Actionmethoden dort implementieren, wo sie sachlich richtig sind. Alles andere wird unübersichtliches Gefrickel.
219
SmartBooks
Objective-C und Cocoa – Band 2
220
View-Controller-Bindung
3
Sie haben in den beiden ersten Kapiteln das Arbeiten mit den eigenen Views gelernt, sowohl hin zum Benutzer als auch zurück von ihm. Das ist aber nur die Eintrittskarte in Ihr Programm. Auf der nächten Ebene müssen wir uns anschauen, wie man Daten vom Controller holt und wieder dort deponiert. Dabei geht es um Verantwortungsteilung. Und wir haben hier sogar drei Ebenen der Verantwortung: die einzelnen Stapel als lokaler Herrscher, den View für den Spieltisch als Herrscher mit Überblick und den Controller als Herrscher über die Spielregeln und das Model. Kernpunkt ist hier also, wie man Verantwortung auf verschiedene Objekte sinnvoll verteilt.
SmartBooks
Objective-C und Cocoa – Band 2
View-Controller-Anbindung Wieder kennen Sie diese Thematik aus dem ersten Band. Dort hatten wir schon Views mit Daten befeuert und welche von diesen erhalten. Auch das Drag & Drop hatten wir mittels eines Delegates schon implementiert – nur eben controllerseitig. Die Architektur war viewseitig bereits von Cocoa vorbereitet, und wir benutzten diese nur noch. Ganz getreu dem Motto des ersten Bandes: Lerne Cocoa benutzen. Hier machen wir uns nun Gedanken, welche Schnittstelle ein View seinem Con���� troller anbietet, damit dieser wiederum mit dem Model kommunizieren kann.
Einleitung Grundsätzlich geht es also um zwei Dinge: den Fluss der Daten sicherzustellen und Nutzeraktionen zu implementieren. Man nennt das eben »Datenfluss« und »Kontrollfluss«. Außerdem kümmern wir uns darum, die bestehende Hierachie der Views ordentlich zu kaspeln.
Model erstellen HILFE Sie sollten sich ein gegenüber dem letzten Kapitel abgespecktes Projekt als Projekt »Card Game 46« von der Webseite herunterladen. Bevor wir aber richtg loslegen können, müssen wir unsere Modellklasse erstellen. Wir halten die Sache einfach und verwenden eine für einen Kartenstapel (CardStack) und einen für eine einzelne Karte (Card). Erzeugen Sie also bitte die neue Klasse CardStack, wobei Sie als Vorlage Objective-C class | NSObject wählen. Sie enthält nicht viel: @interface CardStack : NSObject { NSArray* cards; } @property( copy ) NSArray* cards; @end
Die entsprechende Implementierung:
222
Kapitel 3
View-Controller-Bindung
@implementation CardStack @synthesize cards; -(id)init { self = [super init]; if( self ) { self.cards = [NSArray array]; } return self; } - (void) dealloc { self.cards = nil; [super dealloc]; } @end
Erzeugen Sie entsprechend eine Card-Klasse (Objective-C class | NSObject), der Sie drei Eigenschaften geben: @interface Card : NSObject { NSUInteger value; NSNumber* suit; BOOL disclosed; } @property NSUInteger value; @property( copy ) NSNumber* suit; @property BOOL disclosed; @end
Die unterschiedliche Modellierung der beiden Eigenschaften value und suit als Integer bzw. NSNumber-Instanz ist durchaus Absicht, damit wir Beispiele für verschiedene Fälle erhalten. Die Eigenschaft disclosed bestimmt lediglich, ob die entsprechende Karte aufgedeckt wurde.
223
SmartBooks
Objective-C und Cocoa – Band 2
Auch diese Implementierung gestaltet sich einfach: @implementation Card @synthesize value, suit, disclosed; - (NSString*)description { NSString* text; switch( [self.suit unsignedIntegerValue] ) { case 0: text = [NSString stringWithFormat:@"%C", 0x2662]; break; case 1: text = [NSString stringWithFormat:@"%C", 0x2661]; break; case 2: text = [NSString stringWithFormat:@"%C", 0x2660]; break; case 3: text = [NSString stringWithFormat:@"%C", 0x2663]; break; default: text = @"?"; break; } text = [NSString stringWithFormat:@"%@%d", text, self.value]; if( self.disclosed ) { text = [text stringByAppendingString:@"+"]; } else { text = [text stringByAppendingString:@"-"]; } return text; } - (id)init {
224
Kapitel 3
View-Controller-Bindung
self = [super init]; if( self ) { self.value = 0; self.suit = [NSNumber numberWithInteger:0]; self.disclosed = NO; } return self; } - (void) dealloc { self.value = 0; self.suit = nil; self.disclosed = NO; [super dealloc]; } @end
TIPP Man mag sich fragen, warum wir auch Eigenschaften, die nicht Objekte sind, im -dealloc auf 0 setzen. Aber bedenken Sie bitte, dass möglicherweise der Setter noch anderes unternimmt, als nur die Instanzvariablen zu setzen. Daher kann dies tunlich sein, wenn Aufräumarbeiten zu erwarten sind. Dies gilt umso mehr, wenn es sich um Eigenschaften der Superklasse handelt. Ein bisschen schräg ist das freilich deshalb, weil 0 nicht Nichts bedeutet. Bools und Ganzzahlen kennen allerdings keinen Standard für Nichts (Fließkommazahlen: NaN, Objekte: nil), so dass üblicherweise die 0 so behandelt wird, auch wenn das eigentlich nicht richtig ist. Wir legen nunmehr in -awakeFromNib des Applikationsdelegates die Stapel an. Dazu benötigen wir zunächst eine Änderung im Header: @interface AppDelegate : NSObject { NSArray* stacks; IBOutlet TableBaizeView* tableBaizeView; } @property( copy ) NSArray* stacks; @end
225
SmartBooks
Objective-C und Cocoa – Band 2
Bitte öffnen Sie gleich MainMenu.xib im Interface Builder und verbinden Sie das neue Outlet. Das vergisst man sonst leicht. Kommen wir zur Implementierung, wobei wir uns dreier Hilfsmethoden bedienen: #import "AppDelegate.h" #import "Card.h" #import "CardStack.h" @implementation AppDelegate @synthesize stacks; - (NSMutableArray*)cardsHeap { NSUInteger setIndex; NSUInteger suit; NSUInteger value; NSMutableArray* heap = [NSMutableArray arrayWithCapacity:104]; for( setIndex = 0; setIndex < 4; setIndex++ ) { for( suit = 1; suit < 3; suit++ ) { for ( value = 1; value < 14; value++ ) { Card* card = [[[Card alloc] init] autorelease]; card.value = value; card.suit = [NSNumber numberWithInteger:suit]; card.disclosed = NO; [heap addObject:card]; } } } return heap; } …
Hier werden also vier Sätze an Karten in den Farben Herz bzw. Pik erzeugt und in einem Array gespeichert.
226
Kapitel 3
View-Controller-Bindung
… - (void)shuffleCardsHeap:(NSMutableArray*)heap { // mischen srandom( 9811 ); NSUInteger count = 10000; while( count-- > 0 ) { NSUInteger swapIndex = random() / (float)RAND_MAX * 103; [heap insertObject:[heap lastObject] atIndex:swapIndex]; [heap removeLastObject]; } } …
Diese werden dann gemischt. Dazu bedienen wir uns eines Zufallsgenerators. Gestartet wird dieser mit einem Initialwert durch die Funktion srandom() (start randomize). Die erzeugte Folge von Pseudozufallszahlen ist immer gleich, wenn sich der Startwert nicht ändert. Dies hat Vorteile für das Debugging, da wir bestimmte Situationen erzwingen können. Würden wir später das Spiel ausliefern, so ist der konstante Wert 9811 durch einen variablen zu ersetzen. Meist nimmt man einfach eine Zeit, etwa die aktuelle Uhrzeit. Mit random() wird dann die jeweils nächste Pseudozufallszahl geholt. Hierbei handelt es sich um einen Integer, der entsprechend auf 1.0 skaliert und dann mit der Anzahl der Karten multipliziert wird. Danach liegen die Pseudozufallszahlen in einem Bereich von 0 bis 103 (wegen der insgesamt 104 Karten). … - (NSArray*)stacksWithDealtCards:(NSMutableArray*)heap { NSUInteger stackIndex; NSMutableArray* newStacks; CardStack* stack; NSMutableArray* cards; // Stapel erzeugen newStacks = [NSMutableArray arrayWithCapacity:12]; for( stackIndex = 0; stackIndex < 12; stackIndex++ ) { CardStack* stack = [[[CardStack alloc] init] autorelease]; [newStacks addObject:stack]; } …
227
SmartBooks
Objective-C und Cocoa – Band 2
Diese Schleife erzeugt ein Array mit 12 Arrays für die jeweiligen Stapel. Der Targetstapel bleibt leer, der Depotstapel muss allerdings mit insgesamt 50 Karten befüllt werden. In unserem Model erklären wir den zehnten Stapel zum Depot: … // 50 Karten auf das Depot, im Model Stapel mit Index 10 stack = [newStacks objectAtIndex:10]; cards = [NSMutableArray arrayWithCapacity:50]; NSUInteger cardIndex; for( cardIndex = 0; cardIndex < 50; cardIndex++ ) { Card* card = [heap lastObject]; [heap removeLastObject]; [cards addObject:card]; } stack.cards = cards; …
Entsprechend den Regeln des Spiels werden jetzt die übrigen Karten auf den Stapeln verteilt, wobei die jeweils letzte offen liegt. … // die restlichen Karten bis auf 10 Karten verteilen stackIndex = 0; while( [heap count] > 10 ) { Card* card = [heap lastObject]; [heap removeLastObject]; CardStack* stack = [newStacks objectAtIndex:stackIndex]; cards = [stack mutableArrayValueForKey:@"cards"]; [cards addObject:card]; if( stackIndex == 9 ) { stackIndex = 0; } else { stackIndex++; } } …
Beachten Sie bitte hier, dass wir die Methode -mutableArrayValueForKey: verwenden, um einzelne Karten auf die jeweiligen Stapel zu verteilen (Band I, S. 430 ff.)
228
Kapitel 3
View-Controller-Bindung
Machen wir weiter in unserer Source: … // Die letzten 10 Karten aufgedeckt verteilen for(stackIndex = 0; stackIndex < 10; stackIndex++ ) { Card* card = [heap lastObject]; [heap removeLastObject]; card.disclosed = YES; stack = [newStacks objectAtIndex:stackIndex]; cards = [stack mutableArrayValueForKey:@"cards"]; [cards addObject:card]; } return newStacks; } …
Diese Hilfsmethoden können wir im -init dazu benutzen, unsere Instanz mit Karten herzustellen: … - (id)init { self = [super init]; if( self ) { // Karten erzeugen, mischen und austeilen NSMutableArray* heap = [self cardsHeap]; [self shuffleCardsHeap:heap]; self.stacks = [self stacksWithDealtCards:heap]; } return self; } - (void) dealloc { self.stacks = nil; [super dealloc]; } @end
229
SmartBooks
Objective-C und Cocoa – Band 2
Hiermit haben wir unser Model erzeugt. Sehen werden Sie davon freilich auf dem Bildschirm noch gar nichts, da die Daten noch nicht an den View gelangen. Das folgt jetzt:
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 47« von der Webseite herunterladen.
Datenfluss Ihnen sind bereits im ersten Band die grundsätzlich bestehenden Möglichkeiten des Datenflusses zwischen View und Controller mitgeteilt worden: Accessoren Mittels Accessoren kann man Eigenschaften gezielt setzen. Dies ist lohnend, wenn es sich um eine einzelne Eigenschaft wie den Wert in einem Textfeld oder den Status einer Check-Box handelt. Ungünstig ist es indessen, wenn der View eine Vielzahl von Werten anzeigt, etwa die Zeilen in einem Tableview, da es nicht notwendig ist, sämtiche Zeilen zu setzen, wenn ohnehin nur ein paar Zeilen auf dem Bildschirm sichtbar sind. Bisher machen wir das so, was wir also ändern sollten. Model property
value = property
Controller
setProperty: value
View property
Bei Accessoren werden die Daten in den View gesetzt.
Datasourcen Eine Art, den Datenfluss zu implementieren, kennen Sie ebenfalls bereits aus Band 1 (S. 407): Die Datasource liefert auf Anfrage des Views die entsprechenden Daten. Hier kann der View selbst entscheiden, welche Daten überhaupt relevant sind. Model
Controller
reload getX
Bei einer Data-Source holt sich der View die Daten ab.
230
View
Kapitel 3
View-Controller-Bindung
Bindings Gewissermaßen die Königsdisziplin stellt die Unterstützung von Bindings dar. Hierbei verhalten sich Eigenschaften verschiedener Instanzen synchron. Wie versprochen werden wir selbst Bindingsunterstützung implementieren. Model
Controller
View
Bei Bindings werden Eigenschaften aneinanergekettet.
Kontrollfluss Das zweite Problem, um das wir uns kümmern müssen, liegt im Kontrollfluss. So verhält es sich derzeit so, dass die Kartenstapel immer dann aufblinken, wenn eine Karte auf einen Stapel gezogen wird. Dies ist nicht richtig, da das nur geschehen soll, wenn die gezogenen Karten tatsächlich auf dem Stapel abgelegt werden dürfen. Außerdem geschieht noch nichts. Wir könnten dem View freilich ein Standardverhalten beibringen, welches einfach die Karten stets verschiebt. Richtig weiter bringt uns das aber nicht. Wir wollen natürlich ein passendes Verhalten für das Spiel programmieren. Das Problem liegt darin, dass die Spielregeln nur vom Controller gekannt werden. Auch hier müssen wir also eine View-Controller-Anbindung herstellen. Dies erledigen wir am Ende des Kapitels über Delegating.
GRUNDLAGEN Eine andere Möglichkeit bestünde freilich darin, die Viewklassen Table BaizeView und CardStackView abzuleiten und dort das angepasste Verhalten zu implementieren. Sie sehen hier recht deutlich, dass Delegating ein (besserer) Ersatz für Subclassing ist: Die Verbesserung liegt darin, dass unser Delegate anders als eine Subklasse nicht »in die Ausgangsklasse schaut« und daher nicht die einzelnen Methoden kennt. Die Spezialisierung des Verhaltens erfolgt also extern über eine definierte API. Dadurch bleibt die Kapselung erhalten (Black-Boxing gegenüber White-Boxing).
Viewhierarchien Bereits bisher haben wir uns mit Viewhierarchien beschäftigt. Dies war bisher einfach zu handhaben, da sowohl beim Zeichnen als auch bei der Entgegennahme von 231
SmartBooks
Objective-C und Cocoa – Band 2
Events jedes View seine Hausaufgaben isoliert machen konnte. Das können wir nicht mehr halten, weil etwa Verschiebeoperationen die »Kommunikation« zwischen den Views benötigen. Wir werden, obwohl und gerade weil dies die Implementierung schwieriger macht, aber bei dieser Struktur bleiben. Unser Controller wird alleine mit dem TableBaizeView kommunizieren. Sie lernen daher gleichermaßen im Vorbeiflug den Umgang mit solchen Strukturen.
Data-Source Bei einer Data-Source handelt es sich ja um eine Instanz, die die Daten für eine andere Instanz, eben meist ein View, liefert. Die Kommunikation mit der DataSource läuft über ein vorher definiertes Protokoll, welches in der Regel einen bestimmten Satz an notwendgen Methoden anthält. Außerdem werden wir noch Konstanten für die verschiedenen Stapel definieren.
AUFGEPASST Natürlich kann man sich denken, dass wir zweimal – in dem Model und in der Viewstruktur – Arrays haben, die ab 0 zählen, und daher die Reihenfolge im Model einfach der im View entspricht. Aber bedenken Sie, dass das nicht zwingend so sein muss, weil wir uns in unterschiedlichen Schichten bewegen. Sauberer ist es daher, so zu tun, als ob man wechselseitig von nichts wüsste. Der Controller muss das dann verkleben.
Data-Source anbieten Definieren wir uns das zunächst im Header von TablebaizeView, da es ja unser Designanspruch war, die einzelnen Stapel dahinter zu verbergen. Vor dem Interface von TableBaizeView definieren wir das Protokoll. @class CardStackView; @class TableBaizeView; #pragma mark extern const extern const extern const extern const …
Data-Source-Protokoll NSUInteger playerStacksMinIndex; NSUInteger playerStacksCount; NSUInteger depotStackIndex; NSUInteger targetStackIndex;
Diese globalen Konstanten dienen nur dazu, die Stapel sicher zu identifizieren.
232
Kapitel 3
View-Controller-Bindung
… @protocol TableBaizeViewDataSource - (id)tableBaizeView:(TableBaizeView*)view stackWithIndex:(NSUInteger)index; … AppDelegate stacks
CardStack cards
tableBaizeView: stackWithIndex:
stack-ID CardStack cards
…
Das Delegate wird nach dem Stapel aus dem Model befragt.
Mit dieser Methode holt sich der View die einzelnen Stapel ab. Es ist dabei wichtig zu verstehen, dass sich der View natürlich nicht darauf verlassen kann, dass diese Stapel irgendeiner Klasse angehören. Vielmehr dient die zurückgegebene Instanz lediglich als Identifier, mit dem bei weiteren Anfragen des Views an seine DataSource der Stapel bestimmt werden kann: … - (NSUInteger)tableBaizeView:(TableBaizeView*)view countOfCardsOnStack:(id)stack; - (NSUInteger)tableBaizeView:(TableBaizeView*)view countOfNonDisclosedCardsOnStack:(id)stack; …
233
SmartBooks
Objective-C und Cocoa – Band 2
CardStack cards
Card value suit disclosed Card value suit disclosed
AppDelegate stacks
tableBaizeView: countOfCardsOnStack:
stack-ID
… cards-count
…
Da nunmehr der Stapel bekannt ist, kann eine Abfrage an diesen (Anzahl der Karten) gerichtet werden.
Hier sehen Sie das System: Der vorhin gelieferte Stapel wird jetzt als Parameter übergeben, um die Anzahl der Karten bzw. zugedeckten Karten auf dem Stapel zu bestimmen. … - (id)tableBaizeView:(TableBaizeView*)view cardWithIndex:(NSUInteger)index onStack:(id)stack; …
CardStack cards
Card value suit disclosed Card value suit disclosed
…
AppDelegate stacks
tableBaizeView: cardWithIndex: onStack:
…
card-ID
In dem nächsten Schritt erfolgt die Abfrage nach den einzelnen Karten.
234
0…cards-count stack-ID
Kapitel 3
View-Controller-Bindung
Dasselbe System wenden wir bei den Karten auf einem Stack an. Der Rückgabewert ist der Parameter für die letzten Methoden: … - (NSUInteger)tableBaizeView:(TableBaizeView*)view valueOfCard:(id)card; - (NSUInteger)tableBaizeView:(TableBaizeView*)view suitOfCard:(id)card; @end …
CardStack cards
Card value suit disclosed Card value suit disclosed
AppDelegate stacks
tableBaizeView: suitOfCard:
card-ID
…
suit …
Mit der Karte wiederum werden deren Eigenschaften ermittelt.
Die letzten Methoden liefern dann schließlich den Wert der einzelnen Karte. Wir haben jetzt also das Protokoll definiert. Wir müssen es auf der Seite der DataSource (AppDelegate) implementieren und auf der Seite (TableBaizeView) nutzen. Das Protokoll benutzen wir zur Typisierung der Delegatevariablen: … @interface TableBaizeView : NSView { … id dataSource; } @property( assign ) id dataSource; @end …
Wir legen uns also zunächst eine Eigenschaft an, die später die Data-Source beherbergt. Bitte beachten Sie, dass Data-Sourcen kein retain erhalten, da ansonsten ein Retain-Zyklus entstünde. 235
SmartBooks
Objective-C und Cocoa – Band 2
In TableBaizeView.m definieren wir die im Header versprochenen Konstanten: const const const const
NSUInteger NSUInteger NSUInteger NSUInteger
playerStacksMinIndex = 0; playerStacksCount = 10; depotStackIndex = 10; targetStackIndex = 11;
@implementation TableBaizeView
und erstellen uns noch entsprechende Accessoren in TableBaizeView.m. @implementation TableBaizeView @synthesize dataSource;
GRUNDLAGEN Wie in der Referenz ausgeführt wird, können Sie mithilfe der Methode -conformsToProtocol: auch zur Laufzeit prüfen, ob eine Instanz auf ein Protokoll hört. Das ist hier nicht erforderlich, da wir typisieren können. Es erspart uns zudem eine Ungereimtheit des gcc. Sie werden sich bei dem Delegate noch darüber ärgern. Hier aber erst einmal zum Verstehen der Hauptsache das System von Protokollen. Entsprechend setzen wir die Data-Source in AppDelegate.m in -awakeFromNib: - (void)awakeFromNib { [tableBaizeView setDataSource:self]; } - (id)init
Data-Source implementieren Wenn Sie das Programm übersetzen, werden Sie bemerken, dass eine Fehlermeldung des Compilers erscheint. Er beschwert sich – zu Recht –, dass das Protokoll nicht implementiert ist. Um das zu vermeiden, fangen wir zunächst an, die Methoden im Application-Delegate zu implementieren. Dazu kopieren wir einfach das Protokoll aus TableBaizeView.h in unsere Implementierung AppDelegate.m und füllen diese erst einmal recht primitiv auf:
236
Kapitel 3
View-Controller-Bindung
@synthesize stacks; - (id)tableBaizeView:(TableBaizeView*)view stackWithIndex:(NSUInteger)index { return [self.stacks objectAtIndex:index]; } - (NSUInteger)tableBaizeView:(TableBaizeView*)view countOfCardsOnStack:(id)stack { CardStack* cardStack = (CardStack*)stack; return [cardStack.cards count]; } - (NSUInteger)tableBaizeView:(TableBaizeView*)view countOfNonDisclosedCardsOnStack:(id)stack { return 0; } - (id)tableBaizeView:(TableBaizeView*)view cardWithIndex:(NSUInteger)index onStack:(id)stack { return [[(CardStack*)stack cards] objectAtIndex:index]; } - (NSUInteger)tableBaizeView:(TableBaizeView*)view valueOfCard:(id)card { return [(Card*)card value]; } - (NSUInteger)tableBaizeView:(TableBaizeView*)view suitOfCard:(id)card; { return [[(Card*)card suit] integerValue]; }
Zum einen fällt das Casting (CardStack*, Card*) auf. Es ist in den meisten Fällen nicht notwendig, sorgt aber dafür, dass wir keine Tippfehler machen. Außerdem – 237
SmartBooks
Objective-C und Cocoa – Band 2
deshalb bevorzugen wir es – ist es eine Verdeutlichung an den Leser. Man könnte auch daran denken, einen Laufzeitcheck zu implementieren, der etwa so aussähe: if( [stack isKindOfClass:[CardStack class]] ) { CardStack* cardStack = stack; // id an Klasse return [cardStack.cards objectAtIndex:index]; }
Da wir allerdings ja selbst die Objekte liefern, die in der Parameterliste auftauchen, ist das nicht notwendig. Außerdem sei noch erwähnt, dass bei einem solchen Casting die Dot-Notation Probleme bereitet. Dies liegt daran, dass der Punkt stärker bindet als das Casting. Eine Zeile wie [(CardStack*)stack.cards objectAtIndex:index];
sieht der Compiler als [(CardStack*)(stack.cards) objectAtIndex:index];
Er wendet also das Casting auf den gesamten Ausdruck stack.cards an, geht demnach davon aus, dass der Rückgabewert des Getters eine CardStack-Instanz sein soll. Hierbei handelt es sich um ein Array von Karten (NSArray). Wir wollen aber, dass nur stack diesen Typen erhält. Wer daher die Dot-Notation verwenden möchte, muss das entsprechend klammern (oder eine typisierte Zwischenvariable verwenden): return [((CardStack*)stack).cards objectAtIndex:index];
Wenn Sie jetzt das Programm übersetzen, werden Sie aber immer noch enttäuscht sein: Die Übersetzung wird immer noch abgebrochen, da der Compiler glaubt, unsere Data-Source (AppDelegate) würde das Protokoll nicht implementieren. Um dies offiziell zu machen, müssen wir das in AppDelegate.h explizit bekannt machen: #import #import "TableBaizeView.h" … @interface AppDelegate : NSObject< TableBaizeViewDataSource > { … }
238
Kapitel 3
View-Controller-Bindung
Damit aber das Protokoll bekannt ist, müssen wir den entsprechenden Header importieren.
GRUNDLAGEN Auch bei dem oben angesprochenem Laufzeittest mit conformsToProtocol: verhält es sich so, dass nur die explizite Einhaltung des Protokolles geprüft wird. Werden also nur die Methoden implementiert, erhält man NO als Antwort.
TIPP Dies erhöht natürlich die Anzahl der Abhängigkeiten der verschiedenen Dateien unseres Projektes. Es ist daher keine schlechte Idee, das Protokoll in eine eigene Datei zu packen. Das Protokoll an sich ändert sich ja seltener als das Klasseninterface. Allerdings sind so Klasse und benutztes Protokoll schön zusammengepackt. Um sich eine reine Protokolldatei zu erstellen, erzeugen Sie sich einfach eine neue Datei von der Vorlage Objective-C class und entfernen die .m-Datei wieder. Ein Protokoll hat ja keine Implementierung. Den Header passen Sie entsprechend an. Jetzt passiert zwar auf dem Bildschirm noch nichts Weltbewegendes, weil TableBaizeView die Data-Source noch nicht benutzt, aber immerhin ist unser Übersetzungsfehler weg. Wir haben jetzt also auf der Seite der Data-Source das Notwendige erledigt.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 48« von der Webseite herunterladen.
Data-Source nutzen Wir müssen also noch auf der Seite des TableBaizeVews die Data-Surce nutzen. Wir nehmen zunächst eine einfache Implementierung vor, die wir dann anhand der Möglichkeiten einer Data-Source verbessern. Einfache Implementierung Öffnen Sie TableBaizeView.h und geben Sie dort zunächst zwei Methoden bekannt: @interface TableBaizeView : NSView { … }
239
SmartBooks
Objective-C und Cocoa – Band 2
@property( assign ) id dataSource; - (void)reloadDataForStackWithIndex:(NSUInteger)stackIndex; - (void)reloadData; @end
In deren Implementierung benutzen wir einen Accessor, der uns den Zugriff auf die verschiedenen Stapel erleichtert. Bauen Sie diesen zunächst ein: @synthesize dataSource; - (CardStackView*)viewWithStackIndex:(NSUInteger)index { if( index == depotStackIndex ) { return depotView; } else if( index == targetStackIndex ) { return targetView; } else { return [playerViews objectAtIndex:index]; } }
Vor der Initialisierung kommen dann unsere neuen Methoden: - (void)reloadDataForStackWithIndex:(NSUInteger)stackIndex { CardStackView* stackView; stackView = [self viewWithStackIndex:stackIndex]; id source = self.dataSource; if( !source ) { return; } id stack = [source tableBaizeView:self stackWithIndex:stackIndex]; NSUInteger cardsCount = [source tableBaizeView:self countOfCardsOnStack:stack]; NSMutableArray* cards = [NSMutableArray arrayWithCapacity:cardsCount]; NSUInteger cardIndex; NSDictionary* cardDictionary;
240
Kapitel 3
View-Controller-Bindung
for( cardIndex = 0; cardIndex < cardsCount; cardIndex++ ) { id card = [dataSource tableBaizeView:self cardWithIndex:cardIndex onStack:stack]; NSUInteger skalar; skalar = [dataSource tableBaizeView:self suitOfCard:card]; NSNumber* suit = [NSNumber numberWithInteger:skalar]; skalar = [dataSource tableBaizeView:self valueOfCard:card]; NSNumber* value = [NSNumber numberWithInteger:skalar]; cardDictionary = [NSDictionary dictionaryWithObjectsAndKeys: value, @"value", suit, @"suit", nil]; [cards addObject:cardDictionary]; } [stackView setCards:cards]; } - (void)reloadData { if( self.dataSource == nil ) { return; } // player stacks NSInteger playerStacksIndex; NSInteger index; for( index = 0; index < playerStacksCount; index++ ) { playerStacksIndex = playerStacksMinIndex + index; [self reloadDataForStackWithIndex:playerStacksIndex]; } [self reloadDataForStackWithIndex:depotStackIndex]; [self reloadDataForStackWithIndex:targetStackIndex]; } - (id)initWithFrame:(NSRect)frame {
241
SmartBooks
Objective-C und Cocoa – Band 2
Das sieht in -reloadDataForStackWithIndex: etwas mühselig aus, und dies resultiert letztlich daraus, dass wir die erhaltenen Daten zu einem Array mit Diction�������� arys zusammenklamüsern müssen. Man könnte natürlich die Daten erst bei einem -drawRect: abholen und sich so den Zwischenspeicher sparen. Das ist durchaus denkbar, wenn die Data-Source schnell ist. Notfalls kann ja diese cachen. Hier wollen wir aber die typische Aufgabe darstellen, die typunabhängige Formulierung in der API umzusetzen. Die Entitäten im Model, deren Typ wir nicht kennen, werden in ihre Eigenschaften mit Standardtypen (NSArray, NSString, NSNumber, NSUInteger usw.) zerlegt, durch das Protokoll geschleust und auf der anderen Seite wieder mit neuen Typen, den der View kennt, zusammengesetzt. Das Protokoll ist also gewissermaßen die Brandschleuse. Übrigens auch in anderer Hinsicht, die uns noch einmal beschäftigen wird: Auch eine Typumwandlung von NSNumber nach NSUInteger vor der Schleuse und umgekehrt nach der Schleuse findet statt. CardStack cards
Card value=3 suit=1 disclosed=NO Card value=7 suit=2 disclosed=NO
3 1 NO
NSDictionary value=3 suit=1 disclosed=NO
Card value=5 suit=2 disclosed=YES
Model-Typisierung
API
View-Typisierung
Die Data-Source-API zerlegt das Model in Standardteile, die beim View wieder zusammengefügt werden.
242
Kapitel 3
View-Controller-Bindung
GRUNDLAGEN Es liegt auf den ersten Blick nahe, sich das Dictionary zu sparen und einfach Key-Value-Coding auf die zuvor von der Data-Source gelieferte Card-Instanz anzuwenden. Aber das bedeutet freilich, dass wir im View eine Bedingung für die Modellierung aufstellen: Denn die Eigenschaften müssen auf die Schlüssel suit und value lauten. Das ist bei uns zwar der Fall, aber keinesfalls zwingend. Wer weiß, wie irgendein Programmierer des Models die Eigenschaften genannt hat? Derlei Vorschriften für das Model in einem View sind eine Störung des MVC-Musters. Man sollte das also nicht tun. Eine Alternative wäre es, die zu benutzenden Schlüssel dem TableBaizeView mitzuteilen und den TableBaizeView diese Schlüssel dann auf die Card-Instanz anwenden zu lassen. So etwas Ähnliches werden wir auch später machen, wenn wir Bindings verwenden. Die Kommunikation über das Protokoll muss aber erst angestoßen werden. Wir nutzen diese in -awakeFromNib (AppDelegate): - (void)awakeFromNib { … [tableBaizeView setDataSource:self]; [tableBaizeView reloadData]; }
Bitte schauen Sie aber zunächst nach, ob das insoweit funktioniert. Es müssten ja jetzt die ausgeteilten Karten erscheinen.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 49« von der Webseite herunterladen. Optimierung Wichtiger als diese Implementierungsdetails ist uns etwas anderes: Losgegangen waren wir ja von dem Punkt, dass die Verwendung von Data-Sourcen anstelle von Settern dazu führt, nur die benötgte Information abzuholen. Bisher hatten wir jedoch nur Scherereien mit der Verwendung der Data-Source. Jetzt wollen wir die Vorteile umsetzen: Fragen wir uns also, welche Daten wir gar nicht zur Anzeige des Views benötigen. Bei einem Tableview, unserem Vorbild, ist dies einfach: Dort sind gegebenen243
SmartBooks
Objective-C und Cocoa – Band 2
falls Einträge herausgescrollt. Eine solche Situation haben wir hier aber nicht. Aber es verhält sich so, dass wir zugedeckte Karten haben. Von denen muss das View eigentlich nur die Anzahl kennen, nicht ihre einzelnen Daten. Vor diesem Hintergrund optimieren wir den View so, dass er nur noch diese notwendigen Daten abholt. Dementsprechend sind in der Eigenschaft cards auch nur die offenen Karten gespeichert, nicht mehr die geschlossenen. Von denen merken wir uns lediglich die Anzahl. Zunächst bauen wir das CardStackView so um, dass es selbst diese Eigenschaft unterstützt. @interface CardStackView : NSView { NSArray* cards; NSUInteger countOfNonDisclosedCards; //geschlossene Karten … } @property NSUInteger countOfNonDisclosedCards; @property( readonly ) NSUInteger cardsCount;
Die zweite Eigenschaft berechnen wir uns gleich aus den anderen Eigenschaften. Sie kann daher nicht gesetzt werden (readonly). In der Implementierung bauen wir uns Accessoren wegen des Updates des Bildschirminhaltes: @synthesize alignment, draggedCardsCount; - (NSUInteger)countOfNonDisclosedCards { return countOfNonDisclosedCards; } - (void)setCountOfNonDisclosedCards:(NSUInteger)value { countOfNonDisclosedCards = value; [self setNeedsDisplay:YES]; } - (NSUInteger)cardsCount { return [self.cards count] + self.countOfNonDisclosedCards; } …
244
Kapitel 3
View-Controller-Bindung
Wir müssen einige Stellen in der Source anpassen, da die Anzahl der Karten auf einem Stapel ja jetzt nicht mehr der Anzahl der Karten in einem Array entspricht.
HILFE Wenn Ihnen das zuviel Arbeit bedeuten sollte, so können Sie das Projekt auch im Zustand nach der Änderung als Projekt »Card Game 50« von der Webseite herunterladen. - (void)setCards:(NSArray*)value { // Aenderungsrechtecke herausfinden NSRect dirtyRect; // Die Anzahl der vorhandenen Karten und Stauchung holen NSUInteger nonDisclosedCount = self.countOfNonDisclosedCards; NSUInteger oldCount = [cards count] + nonDisclosedCount; NSUInteger newCount = [value count] + nonDisclosedCount; if( (oldJammed || newJammed) && (oldCount != newCount ) ) { … } else { // Anzahl der gemeinsamen Karten NSUInteger count = (oldCount < newCount)?oldCount:newCount; NSUInteger index; for( index = nonDisclosedCount; index < count; index++ ) { NSUInteger cardIndex = index - nonDisclosedCount; id oldCard = [cards objectAtIndex:cardIndex]; id newCard = [value objectAtIndex:cardIndex]; … } … } … } … - (void)drawCardBackInRect:(NSRect)rect { NSBezierPath* path; CGFloat radius = NSWidth( rect ) * edgeRadius;
245
SmartBooks
Objective-C und Cocoa – Band 2
path = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:radius yRadius:radius]; [[NSColor lightGrayColor] setFill]; [path setLineWidth:0.0]; [path fill]; [[NSColor blackColor] setStroke]; [path stroke]; } - (void)drawRect:(NSRect)rect { NSUInteger suit; NSUInteger value; NSUInteger index; // Zeichne verdeckte Karten NSUInteger nonDisclosedCount = self.countOfNonDisclosedCards; NSUInteger count = self.cardsCount; for( index = 0; index < nonDisclosedCount; index++ ) { NSRect cardRect; cardRect = [self rectForCardWithIndex:index count:count getIsJammed:NULL]; [self drawCardBackInRect:cardRect]; } for( ; index < (count - self.draggedCardsCount); index++ ) { NSUInteger arrayIndex = index - nonDisclosedCount; id card = [self.cards objectAtIndex:arrayIndex]; NSRect cardRect; cardRect = [self rectForCardWithIndex:index count:count getIsJammed:NULL];
246
Kapitel 3
View-Controller-Bindung
if( [self needsToDrawRect:cardRect] ) { … } } } … - (NSUInteger)indexOfCardAtLocation:(NSPoint)location { NSUInteger cardCount = self.cardsCount; NSInteger cardIndex; … } - (void)mouseDragged:(NSEvent*)event { // Bitte gehen Sie weiter, hier gibt es nichts zu sehen NSUInteger count = self.cardsCount; if( count == 0) { … // Falls ausserhalb des Stapels: Weg! if( index == -1 ) { return; } // Falls zugedeckte Karte: Weg! if( index < self.countOfNonDisclosedCards ) { return; } // Groesse berechnen NSRect draggedRect = [self rectForCardWithIndex:index count:count getIsJammed:NULL]; … NSUInteger cardIndex; for( drawIndex = index; drawIndex < count; drawIndex++ ) { cardIndex = drawIndex - self.countOfNonDisclosedCards; id card = [self.cards objectAtIndex:cardIndex]; … } …
247
SmartBooks
Objective-C und Cocoa – Band 2
// Gezogene Karten ausblenden self.draggedCardsCount = self.cardsCount - index; [self setNeedsDisplay:YES]; … } … - (NSDragOperation)draggingEntered:(id)info { NSDragOperation op = [info draggingSourceOperationMask]; if( self.cardsCount > 0 ) { self.selectedCardsIndices = [NSIndexSet indexSetWithIndex:self.cardsCount-1]; [self setNeedsDisplay:YES]; } … }
Die bisherigen Änderungen betreffen also vor allem zwei zu erledigende Dinge: Zum einen kann die Gesamtzahl der Karten nicht mehr mittels -count auf das Kartenarray bezogen werden, da dort ja nur noch die offenen liegen. Zum anderen ist der graphische Index für die Karten bei einem Zugriff auf das Array um die geschlossenen (und daher nicht im Array enthaltenen) Elemente zu vermindern. Selbstverständlich müssen wir die Anzahl der geschlossenen Karten noch in TableBaizeView.m besorgen: - (void)reloadDataForStackWithIndex:(NSUInteger)stackIndex { … id stack = [source tableBaizeView:self stackWithIndex:stackIndex]; NSUInteger nonDisclosedCount = [source tableBaizeView:self countOfNonDisclosedCardsOnStack:stack]; [stackView setCountOfNonDisclosedCards:nonDisclosedCount]; NSUInteger cardsCount = [source tableBaizeView:self countOfCardsOnStack:stack]; NSMutableArray* cards = [NSMutableArray arrayWithCapacity:cardsCount]; NSUInteger cardIndex;
248
Kapitel 3
View-Controller-Bindung
NSDictionary* cardDictionary; for( cardIndex = nonDisclosedCount; cardIndex < cardsCount; cardIndex++ ) { … } … }
Endlich ermitteln wir die entsprechenden Daten im Application-Delegate (AppDelegate.m) als unserer Data-Source: - (NSUInteger)tableBaizeView:(TableBaizeView*)view countOfNonDisclosedCardsOnStack:(id)stack { NSUInteger index; CardStack* cardStack = (CardStack*)stack; for( index = 0; index < [cardStack.cards count]; index++ ) { Card* card = [cardStack.cards objectAtIndex:index]; if( card.disclosed ) { break; } } return index; }
AUFGEPASST Unser Model macht also von dem potentiellen Vorteil, nur die aufgedeckten Karten zu halten, keinen Gebrauch, da es immer die kompletten Daten hält, und die Anzahl der zugedeckten Karten aus der Eigenschaft der einzelnen Karten ermittelt wird. Natürlich könnte sich auch der Stack diese Eigenschaft merken. Dann müssten die zugedeckten gar nicht mehr aktuell gehalten werden und könnten etwa bei Core Data ausgelagert werden. Der Vorteil reduziert sich also bei uns in concreto auf eine Verminderung der Kommunikation zwischen dem View und seiner Data-Source. Dennoch ist eine solche Implementierung wichtig. Sie erlaubt es dem Controller bzw. Model, eine kostensparendere Implementierung vorzunehmen, erzwingt also nicht die Vorratshaltung der Karten.
249
SmartBooks
Objective-C und Cocoa – Band 2
Bevor wir das testen, bauen wir noch einen Log ein, um den Effekt zu sehen: - (id)tableBaizeView:(TableBaizeView*)view cardWithIndex:(NSUInteger)index onStack:(id)stack { NSLog( @"Karte mit Index %d", index ); return [((CardStack*)stack).cards objectAtIndex:index]; }
Die Anzahl der Aufrufe geht jetzt von den ursprünglich 104 drastisch auf 10 zurück: >… Karte mit Index 5 >… Karte mit Index 5 >… Karte mit Index 5 >… Karte mit Index 5 > … Karte mit Index 4 …
Das ist eben der Vorteil einer optimierten Data-Source gegenüber dem Setzen des Arrays.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 50« von der Webseite herunterladen.
Umwandlung Ein weiteres Problem haben wir in einer Richtung bereits gelöst. Wir mussten zwischen NSUInteger und NSNumber umwandeln. Das ist aber vergleichsweise langweilig. Außerdem hatten wir eine Umwandlung zwischen einem Array von Objekten mit der Eigenschaft disclosed zur Eigenschaft countOfNonDisclosedCards vorgenommen. Das war schon spannender, weil es strukturell war. Ein letztes Beispiel wollen wir Ihnen noch geben, nämlich die Umwandlung eines Arrays in ein anderes Array. Und zwar stört uns, dass das Depot so viele Karten zeigt. Zum einen sieht man ohnehin nur den Kartenrücken, zum anderen so viele Karten, dass man sie nicht mehr zählen kann. Das ist insbesondere deshalb misslich, weil immer 10 Karten gleichzeitig ausgeteilt werden. Für den Benutzer ist es also eher interessant, wie viele 10er-Stapel noch vorhanden sind. Daher sollten die Karten zusammengeschoben erscheinen, was nichts anderes bedeutet, als dass nur jede zehnte Karte sichtbar ist.
250
Kapitel 3
View-Controller-Bindung
Dies bauen wir einfach in unsere Data-Source ein, indem wir einen vorgetäuschten Stapel liefern.
TIPP Man könnte das natürlich auch auf View-Ebene machen, indem nur jede zehnte Karte gezeichnet wird. Bedenken Sie bitte, dass auch in diesem Falle der Datendurchsatz reduziert ist, da ja – schlau programmiert – der View über die Data-Source auch nur jede zehnte Karte abholen würde. Wir ändern dazu einfach etwas in unserer Data-Source AppDelegate.m: - (NSUInteger)tableBaizeView:(TableBaizeView*)view countOfCardsOnStack:(id)stack { CardStack* cardStack = (CardStack*)stack; if( stack == [self.stacks objectAtIndex:depotStackIndex] ) { return [cardStack.cards count] / 10; } … } - (NSUInteger)tableBaizeView:(TableBaizeView*)view countOfNonDisclosedCardsOnStack:(id)stack { NSUInteger index; CardStack* cardStack = (CardStack*)stack; if( stack == [self.stacks objectAtIndex:depotStackIndex] ) { return [cardStack.cards count] / 10; } … }
251
SmartBooks
Objective-C und Cocoa – Band 2
AUFGEPASST Sie werden vielleicht anmerken, dass wir die übergebenen Indexe nicht wirklich auswerten. In der Tat geht die Data-Source davon aus, dass es sich um zwölf Stapel handelt. Zu alledem wird bei der Initialisierung von einer bestimmten Stapelreihenfolge ausgegangen. Eine sauberere Implementierung würde anhand der im Protokoll bekanntgegebenen Konstanten entsprechende Stapel aufbauen. Dies würde hier jedoch zu noch unübersichtlicherem Code führen, weshalb wir uns das gespart haben. Mit den drei Baustellen – AppDelegate, TableBaizeView und CardStackView – gleichzeitig fordern wir Sie ja bereits heraus. Schauen Sie sich das Ergebnis an: Es ist wesentlich aufgeräumter – und entspricht zudem den Regeln.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 51« von der Webseite herunterladen.
Key-Value-Observing und Bindings HILFE Da wir jetzt mit Bindings als zu Data-Sourcen alternative Datenflusstechnologie beginnen, können Sie die entsprechenden Methoden wieder entfernen. Wir haben dies so gemacht und stellen das bereinigte Projekt als Projekt »Card Game 52« auf der Webseite zur Verfügung. Natürlich erscheinen dort zunächst keine Karten mehr, bis wir die Bindings implementiert haben. Sie sollten das Projekt herunter laden. Diese Code-Diät dient jedoch nur der Übersicht. In einem »echten« Projekt kann man freilich beide Technologien parallel anbieten und sollte das auch tun. Nachdem wir also die Data-Sourcen viewseitig kennengelernt haben, machen wir dasselbe mit den Bindings. Schließlich hatten Sie dies als die in der Regel bequemere Art des Datenflusses schätzen gelernt. Allerdings bedeutet das Angebot von Bindings doch schon deutlich mehr Hirnschmalz. In der Sache geht es ja darum, die Anzeige eines Views synchron zu einem Model zu halten. Dazwischen befindet sich noch ein Bindings-Controller, der in der Regel zwar nicht erforderlich ist, aber die Sache kapselt und uns vor allem mit Selektie252
Kapitel 3
View-Controller-Bindung
rung, Filterung und Selektionsmanagement zusätzliche Funktionalität bietet. Auch dieser bezieht in der Regel wieder seine Daten über Bindings. Bindings liegen also in der Regel zweistöckig vor. Dennoch haben Sie wenig bis gar nichts mit Bindings von Bindingscontrollern zu tun, da diese bereits vorgefertigt sind, und es wenig Gründe gibt, eine Ableitung herzustellen. Selbst wenn man das Verhalten im Einzelnen ändern will, betrifft das nicht die Bindings des Bindings controllers. Langer Rede kurzer Sinn: Die typische Implementierung eigener Bindingsangebote findet in Viewsubklassen statt – so werden wir es denn dann auch machen. Allerdings werden wir ebenfalls einen NSArrayController ableiten. Standardklasse Eigene Modelklasse
BindingsController
Eigene KVC-Implementierung
Eigene Viewklasse
Eigene KVO-Implementierung
Im Model müssen wir KVC implementieren, im View gegebenenfalls ein Bindings-Angebot. Die Controllerschicht bleibt bei Bindings in der Regel unangetastet.
Änderung
Observierter name …
Aktualisierung
Da Sie ja schon mit Bindings gearbeitet haben, kennen Sie bereits den Zweck: Eine gebundene Eigenschaft einer Instanz soll sich synchron verhalten zu einer observierten Eigenschaft einer anderen Instanz. Jede Änderung der observierten Eigenschaft wird also automatisch in die gebundene übernommen. View value …
Die Änderung einer – observierten – Eigenschaft führt zur Synchronisation einer anderen – gebundenen – Eigenschaft.
253
SmartBooks
Objective-C und Cocoa – Band 2
GRUNDLAGEN Dass Bindings in der Regel zwischen den Schichten des MVC-Modelles Anwendung findet, liegt daran, dass diese sich potentiell nicht kennen. Wir hatten ja auch bei der Data-Source das Problem, dass der View nicht wusste, welche Eigenschaften genau Wert und Farbe einer Karte speicherten. Dort haben wir das über einen Satz von Methoden als Brandschleuse gelöst. Innerhalb einer Schicht, etwa unserer verschiedenen Views, kennt man sich meist ganz gut, weshalb Observierung und Bindings nicht notwendig sind. Dennoch kann man sie auch hier einsetzen. Ob das ratsam ist, sei einmal dahingestellt. (Und wird übrigens zwischen den Autoren kontrovers diskutiert.) Da man etwa bei der Programmierung eines Textfeldes, welches einen Wert anzeigt, nicht wissen kann, welcher Wert später angezeigt wird (Alter, Name, Nachname, Beruf usw.), kann man das auch nicht in seinem Code einsetzen. Vielmehr bezeichnet man eben die angezeigte Eigenschaft neutral und verbindet sie später mit der konkreten Eigenschaft im Programm. Man kann sich das dann so vorstellen, dass gleichermaßen die gebundene Eigenschaft durch die observierte ersetzt wird. Observierter name …
View value …
Die gebundene Eigenschaft veschwindet hinter der observierten.
AUFGEPASST Wie Sie bereits aus dem ersten Band wissen, kann der Transport der observierten Eigenschaft jedoch mit verschiedenen Parametern belegt werden. So lassen sich etwa Value-Transfomer einsetzen, die eine Instanz einer anderen Klase liefern. Das ändert zwar etwas an der Kodierung der Information, jedoch nichts daran, dass letztlich dieselbe Information der observierten Eigenschaft die gebundene ersetzt. Daraus folgt dann auch, dass jegliche Änderung der gebundenen Eigenschaft (hier: value) offenkundig sinnlos ist. Es ist daher fehlerhaft, bei Bindings die gebundene Eigenschaft über Setter noch zu setzen. Der Witz an der gesamten Geschichte ist allerdings, dass das Ganze noch nichts mit Bindings zu tun hat. Der Transport der Daten zwischen den Eigenschaften erfolgt mit Key-Value-Coding. Und die Technologie zur Mitteilung nennt sich Key-Value-Observing.
254
Kapitel 3
View-Controller-Bindung
Observierter name …
KVO KVC
View value …
KVO informiert über Änderungen der observierten Eigenschaft und initiiert einen Datenaustausch mittels KVC.
Allerdings muss ja irgendjemand dem View sagen, welche Eigenschaft mit welcher Eigenschaft einer Instanz synchronisiert werden soll. Der Viewprogrammierer wusste das ja gerade nicht. Bisher haben wir diese Angaben im Interface Builder vorgenommen. Der schreibt aber nur eine Datei. Vielmehr wird beim Laden des Nibs die entsprechende Information gelesen und daraufhin durch eine Nachricht an den View der gesamte Mechanismus installiert und in Gang gesetzt. Dieses Verhältnis zwischen View und dem großen Bestimmer, der die Richtung vorgibt, ist dann das Binding. In der Terminologie des Juristen haben wir also ein Drei-Personen-Verhältnis, in der des französischen Filmemachers eine Ménage à trois. Und beide werden Ihnen erzählen, dass das eine heikle Angelegenheit ist, weil man die Beziehungen auseinanderhalten muss. KVC/KVO
Bound
Binder
Bi
nd
ing
s
Observed
Der Binder steuert die Bindung des gebundenen an das observierte Objekt.
Sie sollten sich diese Rollenverteilung einprägen, um im weiteren Text die Rollen auseinanderhalten zu können. Einfach ist das nämlich nicht.
255
SmartBooks
Objective-C und Cocoa – Band 2
Da Bindings ohne Key-Value-Observing nicht existieren könnne, beginnen wir mit letzteren. Hierzu richten wir die Observierung in unserem Controller (AppDelegate.m) ein und werden diese durch den View bearbeiten lassen. In einem weiteren Schritt beseitigen wir diese Schräglage, indem wir für die Views Bindings implementieren.
Key-Value-Observing Wie bereits einige Male erwähnt, sorgt Key-Value-Observing dafür, dass ein gebundenes Objekt eine Nachricht erhält, wenn sich eine observierte Eigenschaft verändert. Allerdings kann man dabei verschiedene Fälle unterscheiden:
•
Wir beobachten ein einzelnes Attribut einer Instanz. Dies ist sozusagen der Grundfall.
Observierter property
Gebundener property
Änderung Im einfachsten Fall haben wir die Synchronisierung zweier benachbarter Instanzen.
•
Wir beobachten ein Attribut einer Instanz, welches sich hinter einer oder mehreren Master-Detail-Beziehungen (Band I, S. 154 f.) versteckt. Dies entspricht im Key-Value-Coding einem Schlüsselpfad anstelle eines einfachen Schlüssels (Band I, S. 424 f.). Der Witz liegt hierin, dass wir auf zwei Arten eine Synchronisierung auslösen können: Austausch der referenzierenden Instanz oder Austausch der Eigenschaft selbst. Daher muss neben der eigentlichen Observierung eine zweite »heimliche« vorgenommen werden. Dies erledigt bereits Cocoa für uns.
Observierter property
Observierter relationship
Änderung
Änderung
Gebundener property
Wird die Bindung durch einen Schlüsselpfad hergestellt, so existieren zwei Arten von Unfällen.
256
Kapitel 3
View-Controller-Bindung
•
Wir beobachten eine To-many-Beziehung. Dies entspricht in unserem Projekt etwa der Stapelhöhe countOfCards. Natürlich kann sich auch hier die Beziehung hinter einem Schlüsselpfad verbergen. Observierter relationship
Gebundener property
Änderung
Die observierte Eigenschaft kann auch eine To-many-Beziehung sein.
•
Wir beobachten die Eigenschaften von Instanzen hinter einer To-many-Beziehung. Dies ist schon deutlich anders: In den vorangegangenen Fällen war nämlich die Anzahl der zu observierenden Instanzen bei der Einrichtung der Observierung bekannt. Hier ist es aber so, dass Instanzen hinter der To-many-Beziehung hinzugefügt oder entfernt weden können, so dass die Anzahl schwankt. Dies bedeutet, dass wir bei Änderung der Beziehung möglicherweise Änderung der Observierungen für die hinteren Instanzen einleiten oder aufgeben müssen.
Observierter property
Änderung
Observierter property
Gebundener property Observierter relationship
Gebundener relationship
Änderung
Gebundener property
Änderung
Die Anzahl der hinteren Observierungen ist variabel.
Es sei im letzten Fall darauf hingewiesen, dass es keine Rolle spielt, ob die Eigenschaft der hinteren Instanzen auf der Seite des Gebundenen wiederum in gebundenen Eigenschaften einer Vielzahl von Eigenschaften resultiert. Vielmehr kann es auch so sein – und darauf kommt es bei der Implementierung an – dass die observierte Eigenschaft einer Vielzahl von Instanzen in eine einzige Eigenschaft mündet. Denken Sie sich etwa nur, dass die hintere Eigenschaft ein Betrag ist, der zu einer Summe aufaddiert wird. Und genau als Beispiel hierfür haben wir einen solchen 257
SmartBooks
Objective-C und Cocoa – Band 2
Fall auch bereits implementiert: Die (einzelne) Eigenschaft countOfNonDisclosedCards wird etwa aus der jeweiligen Eigenschaft disclosed von mehreren Instanzen gebildet. Hierzu kennt bereits Key-Value-Coding aggregierende Operatoren wie @sum (Band I, 470 f.). Man kann so etwas aber auch selbst programmieren, wie wir es eben für obiges Beispiel unternehmen werden. Observierter property Observierter relationship
Änderung
Gebundener property
Änderung
Observierter property
Änderung Entscheidend für die Implementierung ist die Anzahl der Observierten, nicht was am Ende dabei herauskommt.
Attribute Beginnen wir mit dem einfachsten Fall des Attributes. Wir wollen die Farbe des Filzes veränderlich machen. Zunächst erzeugen wir uns seine entsprechende Eigenschaft des TableBaizeViews …: @interface TableBaizeView : NSView { … NSColor* color; } @property( copy ) NSColor* color;
… deren Accessoren wir implementieren …: @implementation TableBaizeView - (NSColor*)color { return color; } - (void)setColor:(NSColor*)value { if( value != color ) { [color release];
258
Kapitel 3
View-Controller-Bindung
color = [value copy]; [self setNeedsDisplay:YES]; } }
… dann beim Zeichnen benutzen wir …: - (void)drawRect:(NSRect)rect { NSColor* darkerColor = self.color; NSColor* lighterColor = [darkerColor highlightWithLevel:0.7]; NSGradient* gradient; … }
… und entsprechend im -initWithFrame: und -dealloc bedienen wir: - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if( self ) { self.color = [NSColor greenColor]; … } return self; } - (void)dealloc { self.color = nil; … }
Im Application-Delegate schaffen wir den Counterpart, auf den sich die Tischfarbe synchronisieren soll: interface AppDelegate : NSObject< TableBaizeViewDataSource > { NSArray* stacks; NSColor* baizeColor; IBOutlet TableBaizeView* tableBaizeView; } @property( copy ) NSArray* stacks; @property( copy ) NSColor* baizeColor;
259
SmartBooks
Objective-C und Cocoa – Band 2
synthesitieren die Accessoren @implementation AppDelegate @synthesize stacks, baizeColor;
und setzen die Farbe in unserem -awakeFromNib: - (id)init { self = [super init]; if( self ) { self.baizeColor = [NSColor blackColor]; … } return self; } - (void)dealloc { self.baizeColor = nil; self.stacks = nil; }
Wir müssen natürlich im Programmlauf die Farbe ändern können, damit die Synchronisierung sichtbar wird. Hier machen wir es uns ganz einfach, und Sie fügen bitte im Interface Builder dem Fenster unterhalb des TableBaizeViews eine Color Well aus der Library hinzu. Deren Value-Binding binden Sie bitte an die neue Eigenschaft unserer AppDelegate-Instanz. Sie können jetzt über die Color-Well die Eigenschaft im Application-Delegate setzen. Wow, wirklich nichts Neues. Observierung einrichten und auflösen Um die Eigenschaft im View hierzu synchron zu halten, müssen wir eine Observierung des Views einrichten. Dies geschieht in -awakeFromNib (AppDelegate): - (void)awakeFromNib { [self addObserver:tableBaizeView forKeyPath:@"baizeColor" options:NSKeyValueObservingOptionNew context:@"colorBinding"]; }
260
Kapitel 3
View-Controller-Bindung
Was wir einrichten, müssen wir auch wieder aufgeben. Dies können wir im -dealloc machen: - (void)dealloc { [self removeObserver:tableBaizeView forKeyPath:@"baizeColor"]; self.baizeColor = nil; … }
Observierung durchführen Wenn Sie allerdings jetzt schon die Sache ausprobieren, also die Farbe über die Color-Well ändern, werden Sie enttäuscht. Es wird lediglich eine Exception geworfen: >… : An -observeValueForKeyPath:ofObject:change:context: message was received but not handled. Key path: baizeColor Observed object: Change: { kind = 1; new = NSCalibratedRGBColorSpace 1 0.47358 0.760136 1; } Context: 0x9060
Wie bereits gesagt, wird bei einer Änderung eine Observierungsnachricht losgeschickt. Und genau diese Nachricht ist »diejenige, welche«. Diese müssen wir also in unserem View implementieren: - (void)observeValueForKeyPath:(NSString*)keyPath ofObject:observed change:(NSDictionary*)change context:(void*)cContext { NSString* context = cContext; // Ist es meine Observierung? if( [context isEqualToString:@"colorBinding"] ) { // neuen Wert abholen und setzen. self.color = [change objectForKey:NSKeyValueChangeNewKey]; return; } else { [super observeValueForKeyPath:keyPath ofObject:observed
261
SmartBooks
Objective-C und Cocoa – Band 2
change:change context:context]; } } - (id)initWithFrame:(NSRect)frame {
Ein paar Worte zur Implementierung: Da ein View potentiell mehrere Bindings haben kann, muss es eine Art der Identifikation geben. Hierzu wird gerne der Kontext-Parameter verwendet. Typisiert ist der als Zeiger auf irgendwas (void*). Dies ist ziemlich C-Stil und ziemlich schrecklich, weil er einerseits untypisiert ist, aber andererseits anders als id noch keinen Sinn hat. Wir typisieren das Ganze daher zunächst als NSString, weil wir einen solchen als Identifier für die Observierung verwenden wollen. Womit wir jedoch beim nächsten Problem sind: Der Einrichter der Observierung (AppDelegate) und das observierende (gebundene) Objekt (TableBaizeView) müssen entsprechend eine Vereinbarung über den Kontext treffen. Wir haben hier einfach zwei Strings (colorBinding) genommen, was natürlich einem mittleren Gebet für die Übereinstmmung in den beiden Sourcen gleichkommt. Man kann dies Problem lösen, indem man wiederum eine Konstante exportiert, wie wir das bereits im Data-Source-Protokoll gemacht haben. Hier wäre das aber ein Tropfen auf den heißen Stein, weil das Problem struktureller Natur ist: So bilden etwa die gleich zu besprechenden Optionen der Observierung und das change-Dcitionary einen inneren Zusammenhang. Langer Rede kurzer Sinn: So funktioniert der Code zwar, aber er ist unter Verstoß der Kapselung strukturiert, weil eine heimliche Vereinbarung vorausgesetzt wird. Man würde etwas benötigen, was vom View aus eine API anbietet, welche der Controller benutzen kann. Ach, das sind ja Bindings. Sic! Deshalb besprechen wir die ja auch im Anschluss. Hier machen wir aber erst einmal mit Key-Value-Observing als Synchronisationsmechanismus weiter. Sie können den Code ausprobieren. Wenn Sie die Farbe über die Color-Well verändern, so wird diese automatisch übernommen. Allerdings bemerken Sie sicherlich, dass bei Programmstart die Farben noch unterschiedlich gesetzt sind.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 53« von der Webseite herunterladen.
262
Kapitel 3
View-Controller-Bindung
Options und Change-Dictionary Der letzte Punkt bietet gleich die Überleitung zur nächsten Überschrift. Bei Einrichtung der Observierung können Sie bestimmen, wann eine Observationsnachricht ausgelöst wird und welche Daten hierbei geliefert werden. Fügen Sie zunächst einen Log in TableBaizeView ein: - (void)observeValueForKeyPath:(NSString*)keyPath ofObject:observed change:(NSDictionary*)change context:(void*)cContext { NSString* context = cContext; // Ist es meine Observierung? if( [context isEqualToString:@"colorBinding"] ) { NSLog( @"stored: %@", self.color ); NSLog( @"change: %@", change ); … } if( [context isEqualToString:@"set"] ) { … } else { … } }
In AppDelegate.m fügen wir der bisherigen Option eine weitere hinzu: - (void)awakeFromNib { self.baizeColor = [NSColor blackColor]; NSKeyValueObservingOptions options; options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self addObserver:tableBaizeView forKeyPath:@"baizeColor" options:options context:@"colorBinding"]; }
Wie Sie sehen, können Optionen mit dem Bit-Oder-Operator kombiniert werden. Starten Sie das Programm, ändern Sie die Farbe und schauen Sie, was als changeDictionary geliefert wird: 263
SmartBooks
Objective-C und Cocoa – Band 2
>… stored: NSCalibratedRGBColorSpace 0 1 0 1 >… change: { kind = 1; new = NSCalibratedRGBColorSpace 1 1 0 1; old = NSCalibratedWhiteColorSpace 0 1; }
Sie können unschwer erkennen, dass drei Schlüssel übergeben werden.
•
kind (NSKeyValueChangeKindKey) wird unabhängig von unseren KVO-Optionen geliefert. Dahinter verbirgt sich eine Instanz von NSNumber. Der Typ ist ein zu NSKeyValueChangeKind umdefinerter NSUInteger, weshalb man ihn mit -unsignedInteger (NSNumber) holen kann. Bei Attributen erhalten wir stets den Wert 1, was der Konstante NSKeyValueChangeSetting entspricht. Dies liegt daran, dass Attribute nur durch einen Setter verändert werden können. Gleichgültig ist dabei, auf welche Weise die Eigenschaft gesetzt wurde (Setter, Dot-Notation, Key-Value-Coding). Ist automatisches Key-Value-Observing ausgeschaltet, so muss allerdings die Observierung durch explizite Nachrichten willChangeValueForKey: bzw. didChangeValueForKey: ausgelöst werden. Dies ist etwa bei Core Data der Fall (Band I, S. 627).
•
new (NSKeyValueChangeNewKey) ist nur vorhanden, wenn die NSKeyValue ObservingOptionNew bei der Einrichtung der Observierung gesetzt wurde.
•
old (NSKeyValueChangeOldKey) ist nur vorhanden, wenn die Option NSKeyValueObservingOld bei der Einrichtung der Observierung gesetzt wurde.
Wir benutzen NSKeyValueChangeNewKey, um den neuen Wert abzuholen. Neben diesen Optionen zur Steuerung der übermittelten Werte existiert seit OS X 10.5 noch die Möglichkeit, den Umfang der Nachrichten festzulegen: - (void)awakeFromNib { self.baizeColor = [NSColor blackColor]; NSKeyValueObservingOptions options; options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior; … }
Bereits beim Programmstart werden Sie erfreut sehen, dass sich der View automatisch aufsynchronisiert. Dies geschieht durch die Option NSKeyValueObserving 264
Kapitel 3
View-Controller-Bindung
OptionInitial, die sofort eine Observierungsnachricht bei Einrichtung der Observierung auslöst. Den praktischen Wert sehen Sie ja nun auf dem Bildschirm: Der Wert wird aus dem App-Delegate bereits beim Start übernommen. Im Log können Sie auch gleich beim Programmstart das Eintreffen der Nachricht erkennen: >… stored: NSCalibratedRGBColorSpace 0 1 0 1 >… change: { kind = 1; new = NSCalibratedWhiteColorSpace 0 1; }
Wenn Sie jetzt die Farbe über die Color-Well ändern, werden zudem zwei Observierungsnachrichten ausgelöst, wie man im Log erkennen kann: >… stored: NSCalibratedWhiteColorSpace 0 1 >… change: { kind = 1; notificationIsPrior = 1; } >… stored: (null) >… change: { kind = 1; new = NSCalibratedRGBColorSpace 1 0 0 1; }
Die erste Nachricht wird versendet, bevor die observierte Eigenschaft verändert wird. Sie hat daher keinen Eintrag für NSKeyValueChangeNewKey, weil es ja noch keinen neuen Wert gibt. Dafür existiert ein Eintrag NSKeyValueChangeNotificationIsPrior. Wir müssten diesen eigentlich abfangen, da wir sonst keinen neuen Wert aus dem Dictionary erhalten und automatisch nil als neue Farbe gesetzt wird. Wir sind hier aber nur zu Lehrzwecken und nehmen das gleich heraus. Die zweite Nachricht ist dann die standardmäßig versendete. Hier gibt es nichts Neues zu bewundern. Bitte entfernen Sie wieder die Optionen NSKeyValueObservingOptionPrior: - (void)awakeFromNib { self.baizeColor = [NSColor blackColor]; NSKeyValueObservingOptions options; options = NSKeyValueObservingOptionNew
265
SmartBooks
Objective-C und Cocoa – Band 2
| NSKeyValueObservingOptionInitial; [self addObserver:tableBaizeView forKeyPath:@"baizeColor" options:options context:@"colorBinding"]; }
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 54« von der Webseite herunterladen. Die Observierung einer Master-Detail-Relationship selbst gestaltet sich ebenso. Sie dürften allerdings kaum in die Verlegenheit kommen: Die Eigenschaft ist dann ja ein Verweis auf eine Entität. Und eine solche lässt sich nicht sinnvoll als Ganzes anzeigen. Wieso sollten Sie also hierauf synchronisieren?
BEISPIEL Im ersten Band hatten Sie die durchaus frickelige Bindung von Pop-Ups kennengerlernt. Vergisst man hier, neben der Bindung an die ausgewählte Instanz ebenfalls eine Bindung für den angezeigten Wert zu setzen, so erscheint die Description der Instanzen (Band I, S. 467). Schauen Sie sich vielleicht dort noch einmal den Unterschied zwischen dem Binding auf die bezogene Entität und einer Eigenschaft der bezogenen Entität an. Mit letzterem geht es weiter:
Attribute hinter Master-Detail-Beziehungen Etwas anderes ist der Fall, wenn sich das angezeigte Attribut hinter einer oder mehreren Master-Detail-Beziehungen verbirgt. Wir haben also zum begehrten Attribut einen Schlüsselpfad. Hier ist es potentiell möglich, dass sich das Attribut ändert, so wie wir es bisher kannten, oder aber die Entität, die das Attribut hält wird ausgetauscht. Sie haben das ja schon in der Übersicht eingangs des Abschnitts gesehen. Erfreulicherweise wird dies allerdings bereits vom System berücksichtigt, so dass wir nichts Abweichendes unternehmen müssen. Um dies zu demonstrieren, führen wir eine kleine Änderung im Code durch; Zunächst entfernen Sie bitte wieder die Color-Well im Interface Builder. Dann erweitern Sie das Projekt um eine weitere Klasse, die wir ColorPreferences nennen und bei der Sie als Vorlage wieder Objective-C class | NSObject nehmen. Die Umsetzung ist denkbar einfach. Wir geben im Header eine Eigenschaft ein:
266
Kapitel 3
View-Controller-Bindung
@interface ColorPreferences : NSObject { NSColor* baizeColor; } @property( copy ) NSColor* baizeColor; @end
Die entsprechende Implementierung: @implementation ColorPreferences @synthesize baizeColor; - (id) init { self = [super init]; if (self != nil) { self.baizeColor = [NSColor blackColor]; } return self; } - (void) dealloc { self.baizeColor = nil; [super dealloc]; } @end
Das Application-Delegate passen wir entsprechend an: @class ColorPreferences; @interface AppDelegate : NSObject< TableBaizeViewDataSource > { NSArray* stacks; ColorPreferences* colors; IBOutlet TableBaizeView* tableBaizeView; } @property( copy ) NSArray* stacks; @property( retain ) ColorPreferences* colors; @end
Wir erzeugen jetzt also eine eigene Instanz, die verschiedene Farben halten kann. Wir benutzen hier freilich nur eine, weil es ja nur um die Demonstration geht. Entsprechend müssen wir – wie es auch bei einem Binding im Interface Builder der Fall wäre – den Schlüsselpfad ändern. Außerdem haben wir jetzt zwei Möglichkeiten der Änderung: 267
SmartBooks
Objective-C und Cocoa – Band 2
#import "CardStack.h" #import "ColorPreferences.h" @implementation AppDelegate @synthesize stacks, colors; … - (void)awakeFromNib { // Einrichtung mit Initialisierung NSKeyValueObservingOptions options; options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial; [self addObserver:tableBaizeView forKeyPath:@"colors.baizeColor" options:options context:@"colorBinding"]; // Aenderung der gespeicherten Farbe NSColor* newColor = [NSColor greenColor]; self.colors.baizeColor = newColor; // Aenderung der gesamten Entitaet ColorPreferences* colorPrefs; colorPrefs = [[[ColorPreferences alloc] init] autorelease]; colorPrefs.baizeColor = [NSColor redColor]; self.colors = colorPrefs; } - (id)init { self = [super init]; if( self ) { ColorPreferences* colorPrefs = [[[ColorPreferences alloc] init] autorelease]; self.colors = colorPrefs; // Karten erzeugen, mischen und austeilen … } return self; }
268
Kapitel 3
View-Controller-Bindung
- (void)dealloc { [self removeObserver:tableBaizeView forKeyPath:@"colors.baizeColor"]; self.colors = nil; self.stacks = nil; [super dealloc]; }
Im obigen Code wird also einmal eine Observierung ausgelöst, weil dieselbe eingerichtet wird, dann eine, wenn sich das Attribut hinter der Beziehung ändert, und schließlich bei einem Austausch der gesamten Entität, also Änderung der vorderen Beziehung. Dies sehen Sie auch im Log: 2009-07-19 18:16:56.625 Card Game[93068:10b] NSCalibrated RGBColorSpace 0 1 0 1 2009-07-19 18:16:56.628 Card Game[93068:10b] kind = 1; new = NSCalibratedWhiteColorSpace 0 1; } 2009-07-19 18:16:56.629 Card Game[93068:10b] NSCalibrated WhiteColorSpace 0 1 2009-07-19 18:16:56.631 Card Game[93068:10b] kind = 1; new = NSCalibratedRGBColorSpace 0 1 0 1; } 2009-07-19 18:16:56.631 Card Game[93068:10b] NSCalibrated RGBColorSpace 0 1 0 1 2009-07-19 18:16:56.632 Card Game[93068:10b] kind = 1; new = NSCalibratedRGBColorSpace 1 0 0 1; }
stored: change: {
stored: change: {
stored: change: {
Bitte beachten Sie hierbei, dass, auch wenn wir die gesamte Entität austauschen, das change-Dictionary nur das observierte Attribut, also hier eine Farbe, enthält. Es ist für uns also völlig transparent, ob das einzelne Attribut oder die gesamte Entität ausgetauscht wurden. Eines machen Sie bitte auch noch: Setzen Sie einen Breakpoint auf -observeValue ForKeyPath:ofObject:change:context: und starten Sie die Applikation. Sie sehen im Stack, dass die Methode unmittelbar ausgeführt wird, bei der Installation der Observierung wie bei einer Änderung der observierten Eigenschaft.
269
SmartBooks
Objective-C und Cocoa – Band 2
Die Ausführung der Observierungsmethode erfolgt unmittelbar, wenn wir die Eigenschaft ändern.
AUFGEPASST Dieses Verhalten der unmittelbaren Ausführung wid uns später im Kapitel über Nebenläufigkeit noch einmal beschäftigen. Gewöhnen Sie sich schon hier die Vorstellung an, dass die Observierungsmethode so etwas wie ein automatisch ausgeführtes »Unterprogramm« ist.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 55« von der Webseite herunterladen.
270
Kapitel 3
View-Controller-Bindung
To-many-Relation mit NSSet Erhöhen wir die Anforderungen und observieren wir eine To-many-Relation, die mit einem Set gebildet wurde.
AUFGEPASST Das ist übrigens erstaunlich selten notwendig! Üblicherweise werden ja Tomany-Relationen mit einem Array-Controller überwacht. Dieser liefert aber als observierbare Eigenschaft stets ein Array, nämlich arrangedObjects. Dies gilt auch dann, wenn der Array-Controller selbst an ein Set gebunden ist (Band I, S. 444). Da der Array-Controller jedoch schon programmiert ist, müssen wir das nicht mehr tun. Dennoch, zur Vollständigkeit: Zunächst ändern wir in AppDelegate.h die Eigenschaft color in colors, um ein Anwendungsbeispiel zu haben: @interface AppDelegate : NSObject< TableBaizeViewDataSource > { … NSSet* colors; … } … @property( copy ) NSSet* colors;
Sie können jetzt auch die Klasse ColorPreferences wieder aus dem Projekt entfernen, indem Sie in Groups & Files beide Daten löschen. Außerdem kann die Forward-Declaration in AppDelegate.h und der entsprechende Import in AppDele gate.m verschwinden. Wir brauchen dann eine gebundene (synchronisierte) Eigenschaft in unserem View. @interface TableBaizeView : NSView { … NSMutableSet* colors; } - (NSSet*)colors; - (void)setColors:(NSSet*)colors; - (void)addColors:(NSSet*)colors; - (void)removeColors:(NSSet*)colors;
Nein, es ist kein Fehler, dies nicht als Property anzugeben, sondern unsere volle Absicht. Eine Mutable-Porperty ist ja mindestens gefährlich. Wir implementieren 271
SmartBooks
Objective-C und Cocoa – Band 2
hier also einen Subsatz an KVC-Methoden für Sets (Band I, S. 429 f.). Dies muss dann auch in die Implementierung: @implementation TableBaizeView - (NSSet*)colors { return colors; } - (void)setColors:(NSSet*)value { if( colors != value ) { [colors release]; colors = [value mutableCopy]; } } - (void)addColors:(NSSet*)value { [colors unionSet:colors]; } - (void)removeColors:(NSSet*)value { [colors minusSet:value]; }
Setting In der Implementierung AppDelegate.m arbeiten wir dann auf dem Set. Zunächst nehmen wir den Fall, dass man sich das Set abholt, ändert und wieder schreibt. Diese Methode ist die primitivste: - (void)awakeFromNib { // Einrichtung mit Initialisierung NSKeyValueObservingOptions options; options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial; [self addObserver:tableBaizeView forKeyPath:@"colors" options:options context:@"colorBinding"]; // Aenderung mittels Setters: Loeschen NSMutableSet* newSet = [NSMutableSet setWithSet:self.colors]; [newSet removeObject:[toManySet anyObject]]; self.colors = newSet; // Aenderung mittels Setters: Einfuegen newSet = [NSMutableSet setWithSet:self.colors];
272
Kapitel 3
View-Controller-Bindung
[newSet addObject:@"yellow"]; self.colors = newSet; } - (id)init { self = [super init]; if( self ) { self.colors = [NSSet setWithObjects:@"red", @"green", @"blue", nil]; … } return self; } - (void)dealloc { [self removeObserver:tableBaizeView forKeyPath:@"colors"]; … }
Außerdem müssen wir natürlich TableBaizeView.m anpassen: - (void)observeValueForKeyPath:(NSString*)keyPath ofObject:observed change:(NSDictionary*)change context:(void*)cContext { NSString* context = cContext; // Ist es meine Observierung? if( [context isEqualToString:@"colorBinding"] ) { NSLog( @"change: %@", change ); // Gebundene Eigenschaft synchronisieren NSSet* new = [change objectForKey:NSKeyValueChangeNewKey]; self.colors = new; return; } else { … } … }
273
SmartBooks
Objective-C und Cocoa – Band 2
Im -drawRect: müssen wir nun die Farbe selbst setzen, da die zu unseren Experimentierzwecken eingebaute Eigenschaft color verschwunden ist: - (void)drawRect:(NSRect)rect { NSColor* darkerColor = [NSColor greenColor]; … }
Ebenso entfernen Sie bitte ersatzlos die Zuweisungen an die Property in -initWithFrame: und -dealloc. Wenn Sie jetzt das Programm starten, sehen Sie im Log, was Sie wahrscheinlich auch erwartet haben: >… change: { kind = 1; new = {( red, green, blue )}; } >… change: { kind = 1; new = {( green, blue )}; } >… change: { kind = 1; new = {( green, yellow, blue )}; }
Bitte beachten Sie zwei Umstände, die sich daraus ergeben, dass wir Setter zur Änderung der Collection verwenden: Zum einen erhalten wir als Art der Änderung (NSKeyValueChangeKindKey) weiterhin den Wert 1, also NSKeyValueChangeSetting. Dies ist ja auch einfach richtig. Zum anderen wird uns daher das gesamte 274
Kapitel 3
View-Controller-Bindung
neue Set präsentiert. Wir sehen also nicht mehr das einzelne Element, welches eingefügt oder gelöscht wurde. Haben wir etwa den Fall, dass man etwas genau mit der Änderung machen möchte, und den haben wir sogleich, so bedeutet dies, dass wir auf allen Elementen arbeiten müssen, weil wir nicht mehr erkennen können, was genau passiert ist. Das ist natürlich unter Gesichtspunkten des Laufzeitverhaltens nachteilig. Man kann allerdings anhand des bisher im View gespeicherten Wertes oder anhand des Old-Keys im Change-Dictionary ermitteln, was im Endergebnis hinzugefügt oder entfernt wurde. (Das ist nicht exakt dasselbe, weil zwischen Holen und Speichern ein Element hinzugefügt und wieder entfernt worden sein könnte. Das bemerken wir dann nicht, interessiert uns aber eigentlich auch nie.) Bauen wir das mal ein: - (void)observeValueForKeyPath:(NSString*)keyPath ofObject:observed change:(NSDictionary*)change context:(void*)cContext { NSString* context = cContext; // Ist es meine Observierung? if( [context isEqualToString:@"colorBinding"] ) { NSMutableSet* difference; NSLog( @"change: %@", change ); NSSet* new = [change objectForKey:NSKeyValueChangeNewKey]; NSSet* old = [change objectForKey:NSKeyValueChangeOldKey]; difference = [NSMutableSet setWithSet:new]; [difference minusSet:old]; NSLog( @"insert %@", difference ); difference = [NSMutableSet setWithSet:old]; [difference minusSet:new]; NSLog( @"remove %@", difference ); self.colors = new; return; } else { … } }
275
SmartBooks
Objective-C und Cocoa – Band 2
Da wir in diesem Code nach dem Motto »Finden Sie die Fehler im rechten Bild« vorgehen, benötigen wir auch den alten Wert. Daher ändern wir auch in AppDelegate.m die Optionen der Observierung: - (void)awakeFromNib { … options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial; … }
Sie können jetzt nach einem Programmdurchlauf im Log sehen, dass jeweils die Einfügung bzw. Löschung erkannt werden: >… change: { kind = 1; new = {( … )}; } >… insert {( red, green, blue )} >… remove {( )} >… change: { kind = 1; new = {( green, blue )}; old = {( red, green, blue )}; } >… insert {(
276
Kapitel 3
View-Controller-Bindung
)} >… remove {( red )} >… change: { kind = 1; new = {( green, yellow, blue )}; old = {( green, blue )}; } >… insert {( yellow )} >… remove {( )}
Es sei noch erwähnt, dass im ersten Aufruf kein old-Eintrag mitgeliefert wird, so dass wir aus dem Dictionary nil erhalten. Daher ziehen wir nichts (nil) vom ���� Dictionary ab und ziehen umgekehrt die neuen Elemente von nichts (nil) ab. Beides ist erlaubt. Gezieltes Ändern Bereits in Band I (S. 426 ff.) wurde darauf hingewiesen, dass die hier vorgenommene Weise der Änderung (holen, ändern, speichern) bei To-many-Relationships für denjenigen, der ändert, unbequem und für Key-Value-Observing zudem nachteilig ist. Sie haben jetzt bereits eine Ahnung davon, warum dies der Fall ist: Man muss als Observierer herausfriemeln, was sich getan hat. Die Alternative ist -mutableSetValueForKey:. Ändern wir entsprechend unseren Code im AppDelegate: - (void)awakeFromNib { NSSet* toManySet = [NSSet setWithObjects:@"red", @"green", @"blue", nil]; self.colors = toManySet;
277
SmartBooks
Objective-C und Cocoa – Band 2
// Einrichtung mit Initialisierung NSKeyValueObservingOptions options; options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial; [self addObserver:tableBaizeView forKeyPath:@"colors" options:options context:@"colorBinding"]; // Aenderung mittels Setters: Loeschen NSMutableSet* newSet = [self mutableSetValueForKey:@"colors"]; [newSet removeObject:[toManySet anyObject]]; // Aenderung mittels Setters: Einfuegen newSet = [self mutableSetValueForKey:@"colors"]; [newSet addObject:@"yellow"]; }
Wir ändern also gleich das Set und können daher das Zurückschreiben bleibenlassen. Im Log tut sich daraufhin Wundersames: 2009-07-20 16:47:40.465 kind = 1; new = {( red, green, blue )}; } 2009-07-20 16:47:40.479 red, green, blue )} 2009-07-20 16:47:40.480 )} 2009-07-20 16:47:40.482 kind = 3; old = {( red )};
278
Card Game[7269:10b] change: {
Card Game[7269:10b] insert {(
Card Game[7269:10b] remove {( Card Game[7269:10b] change: {
Kapitel 3
View-Controller-Bindung
} 2009-07-20 16:47:40.483 )} 2009-07-20 16:47:40.484 red )} 2009-07-20 16:47:40.486 kind = 2; new = {( yellow )}; } 2009-07-20 16:47:40.486 yellow )} 2009-07-20 16:47:40.487 )}
Card Game[7269:10b] insert {( Card Game[7269:10b] remove {(
Card Game[7269:10b] change: {
Card Game[7269:10b] insert {(
Card Game[7269:10b] remove {(
Es wird jetzt also automatisch mitgeteilt, was als konkrete Einfüge- bzw. Löschoperation passiert ist. Und zwar einerseits über die Art der Änderung (kind), andererseits über die mitgelieferten Werte. Es gilt:
•
Werden Objekte hinzugefügt, so ist der Wert hinter NSKeyValueChangeKindKey gleich NSKeyValueChangeInsertion (2), und unter dem Schlüssel NSKeyValueChangeNewKey erthält man die eingefügten Elemente.
Fügen wir im Modell Instanzen ein, so erhalten wir in der Observierungsmethode ein Set davon.
•
Werden Objekte entfernt, so ist der Wert hinter NSKeyValueChangeKindKey gleich NSKeyValueChangeRemoval (3), und unter dem Schlüssel NSKeyValueChangeOldKey erthält man die entfernten Elemente. 279
SmartBooks
Objective-C und Cocoa – Band 2
Entsprechend werden uns die zu entfernenden Instanzen mitgeteilt.
Mit diesem Wissen können wir unsere Methode etwas optimieren, etwa: - (void)observeValueForKeyPath:(NSString*)keyPath ofObject:observed change:(NSDictionary*)change context:(void*)cContext { NSString* context = cContext; // Ist es meine Observierung? if( [context isEqualToString:@"colorBinding"] ) { NSNumber* kindObject = [change objectForKey:NSKeyValueChangeKindKey]; NSLog( @"change: %@", change ); NSSet* new; NSSet* old; switch( [kindObject unsignedIntegerValue] ) { case NSKeyValueChangeSetting: { NSMutableSet* difference; … self.colors = new; break; } case NSKeyValueChangeRemoval: old = [change objectForKey:NSKeyValueChangeOldKey]; [self removeColors:old]; NSLog( @"remove %@", old ); break;
280
Kapitel 3
View-Controller-Bindung
case NSKeyValueChangeInsertion: new = [change objectForKey:NSKeyValueChangeNewKey]; [self addColors:new]; NSLog( @"insert %@", new ); break; default: break; } return; } else { … } }
Sie sehen schon, dass der Code aussagekräftiger ist. Sie müssen übrigens nicht den Code für Removal und Insertion implementieren. Zwar werden bei diesen Änderungen jeweils nur die eingefügten (new) bzw. entfernten Objekte (old) mitgeteilt. Aber wie bereits oben gezeigt, schmerzt das nicht, da wir nil in den Set-Methoden verwenden dürfen.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 56« von der Webseite herunterladen. Was Sie verstanden haben sollten, ist einfach, dass es bei To-many-Relationen mit Sets andere Fälle als ein einfaches Austauschen des Sets geben kann. Bevor wir voranschreiten, entfernen Sie bitte die Methode -observeValueForKeyPath:ofObject: change:context: aus TableBaizeView.m, wenn Sie wollen. Wir benötigen diese nicht mehr. -awakeFormNib leeren Sie bitte und in -dealloc und -init entfernen Sie den Bezug zur Eigenschaft colors, die sie ebenfalls entfernen. Gleiches gilt für den TableBaizeView. (Vergessen Sie die Accessoren nicht.) - (void)awakeFromNib { } … - (void)dealloc { self.stacks = nil; [super dealloc]; }
281
SmartBooks
Objective-C und Cocoa – Band 2
Zum Abschluss noch eine Tabelle der mitgelieferten Einträge im change-Dictionary. Aus Platzgründen ist das stets gleiche Präfix NSKeyValueChange durch … ersetzt: Kind Alter Wert …KindKey …OldKey …Setting Setzen
Set mit allen alten Instanzen
Neuer Wert …NewKey
Set mit allen neuen Instanzen
…Insertion -/Einfügung
Set mit den eingefügten Instanzen
…Removal Set mit den entfernten Entfernung Instanzen
-/-
To-many-Relation mit Array Mit diesem Vorwissen können wir uns einen noch etwas komplizierteren – und vor allem praktisch wichtigeren – Fall anschauen: Wir haben eine To-many-Relation, die mit einem Array modelliert ist, also sortiert. Dies ist deshalb komplexer, weil jetzt nicht mehr die Information ausreicht, welche Elemente hinzugefügt bzw. entfernt wurden, sondern auch, an welcher Stelle das geschah. Außerdem können Elemente ausgetauscht worden sein.
AUFGEPASST Bedenken Sie bitte, dass bei einem Array – anders als bei einem Set – ein Element merfach vorhanden sein kann. Die Mitteilung, dass ein Element entfernt wurde, sagt uns also noch nicht wo, wenn es mehrfach im Array abgelegt ist. Da wir in unserer Anwendung einen solchen Fall haben, müssen wir auch nicht mehr so experimentell programmieren. Unsere Kartenstapel bestehen ja aus einem Array von Card-Instanzen. Diese observieren wir jetzt, um die Karten zu erhalten. Wechseln Sie zu CardStackView.m und bauen Sie nach den Settern die Observierungsmethode ein: - (void)observeValueForKeyPath:(NSString*)keyPath ofObject:observed change:(NSDictionary*)change context:(id)context { // Ist es meine Observierung? if( [context isEqualToString:@"cardsBinding"] ) {
282
Kapitel 3
View-Controller-Bindung
NSLog( @"change: %@", change ); } else { [super observeValueForKeyPath:keyPath ofObject:observed change:change context:context]; } }
Um Ihnen nicht die Finger wund zu machen, werden wir nur den Target-Stapel entsprechend observieren lassen. Damit wir auf diesen zugreifen können, exportieren wir die Methode zur Ermittlung eines Views in TableBaizeView.h: @interface TableBaizeView : NSView { … } - (CardStackView*)viewWithStackIndex:(NSUInteger)index; @end
Richten wir eine Observierung in AppDelegate.m ein: #import "TableBaizeView.h" #import "CardStackView.h" … - (void)awakeFromNib { CardStack* targetStack = [self.stacks objectAtIndex:targetStackIndex]; NSView* targetView = [tableBaizeView viewWithStackIndex:targetStackIndex]; NSKeyValueObservingOptions options; options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial; [targetStack addObserver:targetView forKeyPath:@"cards" options:options context:@"cardsBinding"]; }
283
SmartBooks
Objective-C und Cocoa – Band 2
- (void)dealloc { CardStackView* targetView = [tableBaizeView viewWithStackIndex:targetStackIndex]; CardStack* targetStack = [self.stacks objectAtIndex:11]; [targetStack removeObserver:targetView forKeyPath:@"cards"]; … }
Ferner implementieren Sie bitte vier Actionmethoden: @synthesize stacks; - (IBAction)addCard:(id)sender { CardStack* stack = [self.stacks objectAtIndex:11]; Card* card = [[[Card alloc] init] autorelease]; NSMutableArray* cards = [stack mutableArrayValueForKey:@"cards"]; card.value = [cards count] + 1; [cards addObject:card]; } - (IBAction)removeLastCard:(id)sender { CardStack* stack = [self.stacks objectAtIndex:11]; NSMutableArray* cards = [stack mutableArrayValueForKey:@"cards"]; if( [cards count] > 0 ) { [cards removeObject:[cards lastObject]]; } } - (IBAction)replaceFirstCard:(id)sender { CardStack* stack = [self.stacks objectAtIndex:11]; NSMutableArray* cards = [stack mutableArrayValueForKey:@"cards"]; if( [cards count] > 0 ) { Card* card = [[[Card alloc] init] autorelease]; card.value = 12; [cards replaceObjectAtIndex:0 withObject:card]; } }
284
Kapitel 3
View-Controller-Bindung
- (IBAction)setStack:(id)sender { CardStack* stack = [self.stacks objectAtIndex:11]; NSMutableArray* newCards = [NSMutableArray array]; NSUInteger value; Card* card; for( value = 1; value < 7; value++ ) { card = [[[Card alloc] init] autorelease]; card.value = value; [newCards addObject:card]; } stack.cards = newCards; }
Sie sehen also, dass diese Methoden so ziemlich alles machen, was man mit einem Array machen kann: Anfügen, entfernen, austauschen und setzen. Publizieren Sie die Methoden im Header: @property( copy ) NSArray* stacks; - (IBAction)addCard:(id)sender; - (IBAction)removeLastCard:(id)sender; - (IBAction)replaceFirstCard:(id)sender; - (IBAction)setStack:(id)sender;
und erzeugen Sie in MainMenu.xib entsprechende Menüeinträge, etwa im EditMenü. Diese verbinden Sie wenig überraschend mit den neuen Actionmethoden. Setzen des Arrays Bitte übersetzen und starten Sie die Applikation. Wir machen ja noch nichts Bedeutsames dort, aber Sie können schon die Auswirkungen unseres Tuns sehen. Bereits beim Programmstart wird ja wegen der NSKeyValueOptionInitial-Option gleich der Startwert des Stapels – hier ist er noch leer – gesetzt: >… change: { kind = 1; new = ( ); }
Wie auch bisher bekommen wir eine Setzanforderung (NSKeyValueChangeSetting). Hinter dem Schlüssel NSKeyValueChangeNewKey verbirgt sich ein leeres Array. Wie erwartet.
285
SmartBooks
Objective-C und Cocoa – Band 2
AUFGEPASST Was passiert eigentlich, wenn das observierte Array noch gar nicht erzeugt ist, die cards-Eigenschaft des Stacks also nil ist? Die naheliegende Lösung, in das change-Dictionary dann nil für den Schlüssel NSKeyValueChange NewKey zu hinterlegen, funktioniert nicht, da Collections kein nil aufnehmen können. Es wird vielmehr die Platzhalterinstanz NSNull hinterlegt (Band I, S. 321). Jetzt fügen Sie bitte über den Menüeintrag für die Actionmethode -addCard: eine Instanz ein. Der Log: >… change: { i ndexes = [number of indexes: 1 (in 1 ranges), indexes: (0)]; kind = 2; new = ( ♢1); }
Als Art wird jetzt wenig überraschend NSKeyValueChangeInsertion angegeben, außerderm – ebenso erwartet – erhalten wir in NSKeyValueChangeNewKey ein Array mit der neuen Karte, ein zugedecktes Karo-As. Daneben ergibt sich aber etwas Neues: Es wird zudem angegeben, an welcher Stelle diese neue Karte eingefügt wurde, hier ist es der Index 0. Den Index erhalten Sie aus dem change-Dictionary mit dem Schlüssel NSKeyValueChangeIndexesKey.
Bei einem Array muss zusätzlich der Index mitgeliefert werden.
286
Kapitel 3
View-Controller-Bindung
Entsprechend verhält es sich mit dem Austauschen: (Sie müssen natürlich zunächst eine Karte hinzufügen, wenn Sie das Programm neu gestartet haben.) >… change: { indexes = [number of indexes: 1 (in 1 ranges), indexes: (0)]; kind = 4; new = ( ♢12); old = ( ♢1); }
Wird ein Element ausgetauscht, so erhalten wir das alte, das neue und den Index der Position.
In NSKeyValueChangeKindKey erhalten wir den Wert NSKeyValueChangeReplace ment, wieder den Wert NSKeyValueChangeIndexesKey, in NSKeyValueChangeNewKey das eingefügte Element und in NSKeyValueChangeOldKey das entfernte. Klar. Löschen wir noch einmal ein Element: >… change: { indexes = [number of indexes: 1 (in 1 ranges), indexes: (0)];
287
SmartBooks
Objective-C und Cocoa – Band 2
kind = 3; old = ( ♢12); }
Wir erhalten jetzt also Position und Element.
Wird ein Element entfernt, so erhalten wir dessen Index und das Element selbst.
Fügen Sie noch ein paar Karten ein (im Beispiel unten 3) und benutzen Sie die letzte Funktion zum Setzen: >… change: { kind = 1; new = ( ♢1-, ♢2-, ♢3-, ♢4-, ♢5-, ♢6); old = ( ♢1-, ♢2-, ♢3); }
288
Kapitel 3
View-Controller-Bindung
HILFE Sie können das Projekt als Projekt »Card Game 57« von der Webseite herutnerladen. Mehrfachänderungen Das leitet zum nächsten Problem der Mehrfachänderungen weiter. Es ist ja möglich, dass wir zwei Karten auf einmal austauschen. Fügen wir eine neue Austauschmethode ein: - (IBAction)replaceFirstAndLastCard:(id)sender { CardStack* stack = [self.stacks objectAtIndex:11]; NSMutableArray* cards = [stack mutableArrayValueForKey:@"cards"]; if( [cards count] > 1 ) { Card* card1 = [[[Card alloc] init] autorelease]; card1.value = 4; Card* card2 = [[[Card alloc] init] autorelease]; card2.value = 6; NSArray* replaceCards = [NSArray arrayWithObjects:card1, card2, nil]; NSMutableIndexSet* indexes = [NSMutableIndexSet indexSet]; [indexes addIndex:[cards count]-1]; [indexes addIndex:0]; NSLog( @"Controller Indexe: %@", indexes ); [cards replaceObjectsAtIndexes:indexes withObjects:replaceCards]; } } - (IBAction)setStack:(id)sender
Bereits zu dieser Methode ist etwas zu sagen: Wie Sie sehen, wird ein Array mit zwei Karten (card1, card2) erzeugt. Ferner erstellen wir uns ein Index-Set. Bereits im ersten Band wurde angesprochen, dass es sich hierbei um eine Klasse handelt, die vor allem in Bezug auf Key-Value-Observing wichtig ist. Die uns gelieferten Indexe sind übrigens in einem Index-Set verpackt. Hier verwenden wir es aber selbst und fügen – in dieser Reihenfolge – als Indexe den der letzten Karte und 0 hinzu. Wichtig ist hierbei, dass ein Index-Set unsortiert ist, also äh, sortiert, also … Es spielt anders als etwa bei Arrays keine Rolle, in welcher Weise Sie einzelne Indexe dem Index-Set hinzufügen. Sie können einen Index auch nicht an einer bestimm-
289
SmartBooks
Objective-C und Cocoa – Band 2
ten Stelle hinzufügen. Vielmehr sortiert es sich selbst. Unabhängig von der Reihenfolge in unserem Code lautet der Index-Set daher immer auf { 0, letzte Karte }. Schließlich tauschen wir die Elemente des Arrays eben an allen Stellen aus dem Index-Set mit den Elementen aus unserem Array aus. Das bedeutet – wiederum unabhängig von der Reihenfolge in unserem Code –, dass das erste Element in unserem Array (card1) an der Stelle 0 eingesetzt wird, das zweite Element (card2) an die letzte Stelle des Zielarrays. Nachdem Sie auch diese Methdoe im Header publiziert haben …: - (IBAction)setStack:(id)sender; - (IBAction)replaceFirstAndLastCard:(id)sender;
… fügen Sie in MainMenu.xib einen entsprechenden Menüeintrag ein und verbinden Sie diesen mit der neuen Methode. Starten Sie die Anwendung und fügen Sie drei Karten hinzu. Dann nutzen Sie bitte die neue Methode. >… Controller Indexe: [number of indexes: 2 (in 2 ranges), indexes: (0 2)] >… change: { indexes = [number of indexes: 2 (in 2 ranges), indexes: (0 2)]; kind = 4; new = ( ♢4-, ♢6); old = ( ♢1-, ♢3); }
Entsprechend bekommen wir hier die Änderung als Denksportaufgabe mitgeteilt. An der Stelle mit dem Index 0 ist ein Karo-As durch eine Karo-Vier zu ersetzen und an der Stelle mit dem Index 2 ein Karo-Drei durch eine Karo-Sechs. Zusammenfassen kann man das in folgender Tabelle, wobei wir den Präfix NSKeyValueChange mit … ersetzt haben:
290
Kapitel 3
View-Controller-Bindung
Kind …KindKey
Alter Wert …OldKey
Neuer Wert …NewKey
…Setting Setzen
Array mit allen alten Instanzen
Array mit allen neu- -/en Instanzen
…Insertion Einfügung
-/-
Array mit den einge- Indexe der eingefügten Instanzen fügten Instanzen
…Removal Entfernung
Array mit den ent- -/fernten Instanzen
…Replacement Array mit den erErsetzung setzten Instanzen
Array mit den ersetzenden Instanzen
Indexe … IndexesKey
Indexe der entfernten Instanzen Indexe der Ersetzungen
Implementierung Hiervon ausgehend können wir das Ganze implementieren. Zunächst holen wir uns einfach die Änderungen ab und bauen lokal ein entsprechendes Array nach. Um die Feinheiten wie zugedeckte Karten kümmern wir uns dann später. Als Vorarbeit zur einfacheren Formulierung erweitern wir zunächst NSDictionary um ein paar Methoden mittels einer Kategorie. Hierzu erzeugen Sie bitte eine neue Klasse aus der Vorlage Objective-C class | NSObject und nennen diese KeyValueBindingExtension. Den Header ändern Sie dann wie folgt: @interface NSDictionary( KeyValueBindingExtension ) - (NSKeyValueChange)keyValueChangeKind; - (id)keyValueChangeNew; - (id)keyValueChangeOld; - (NSIndexSet*)keyValueChangeIndexes; @end
Bitte beachten Sie die Typisierung der zwei mittleren Methoden. Je nach Art der Observierung – Attribut, To-many mit Set bzw. To-many mit Array – können ja Instanzen unterschiedlicher Klassen geliefert werden. Die entsprechende Implementierung: @implementation NSDictionary( KeyValueBindingExtension ) - (NSKeyValueChange)keyValueChangeKind { return [[self objectForKey:NSKeyValueChangeKindKey] unsignedIntegerValue]; }
291
SmartBooks
Objective-C und Cocoa – Band 2
- (id)keyValueChangeNew { return [self objectForKey:NSKeyValueChangeNewKey]; } - (id)keyValueChangeOld { return [self objectForKey:NSKeyValueChangeOldKey]; } - (NSIndexSet*)keyValueChangeIndexes { return [self objectForKey:NSKeyValueChangeIndexesKey]; } @end
Jetzt müssen wir das nur noch in CardStackView.m importieren: #import "BezierPathCardsAddition.h" #import "KeyValueBindingExtension.h"
Als weitere Maßnahme, den Code lesbarer – und performanter – zu machen, ändern wir die Instanzvariable cards in CardStackView in ein Mutable-Array. Jetzt bekommen wir allerdings ein kleines Problem: Wir können nicht einfach die Property auf Mutable-Array ändern, da wir dann eine veränderliche Instanz ausliefern würden, an der jeder herumfummeln kann. Mal abgesehen davon, dass dies unter dem Gesichtspunkt der Kapselung extrem eklig ist, muss bei einer Änderung das View aktualisiert werden. Das wäre nicht mehr gewährleistet. Aus diesem Grund müssen wir die Property löschen und stattdessen zumindest teilweise den Methodensatz von Key-Value-Coding für Arrays implementieren:
GRUNDLAGEN Es sei übrigens am Rande bemerkt, dass es hier nicht darum geht, kompatibel zu Key-Value-Observing zu sein. Denn ein View wird nicht observiert, er observiert selbst. Wir könnten also die Methoden benennen, wie wir gerade lustig sind. Aber wieso? @interface CardStackView : NSView { NSMutableArray* cards; … }
292
Kapitel 3
View-Controller-Bindung
@property( readonly ) NSUInteger cardsCount; // Property cards (to-many relationship) - (NSArray*)cards; - (void)setCards:(NSArray*)cards; - (void) insertCards:(NSArray*)cards inCardsAtIndexex:(NSIndexSet*)indexes; - (void)removeCardsAtIndexes:(NSIndexSet*)indexes; - (void)replaceCardsAtIndexes:(NSIndexSet*)indexes withCards:(NSArray*)cards; @property NSRectEdge alignment;
Beachten Sie hier bitte, dass stets NSArray anstelle von NSMutableArray verwendet wurde, um so die versehentliche Änderung der Instanzvariable zu verhindern. Die entsprechende Implementierung: - (void)setCards:(NSArray*)value { … // Standardaccessor if( value != cards ) { [cards release]; cards = [value mutableCopy]; } } …
Hier legen wir uns ein eigenes Mutable-Array an. Dies hat zwei Gründe: Zum einen ist der Parameter ja als NSArray typisiert. Wir können ihn also nicht unmittelbar zuweisen. Zum anderen ist es möglich, dass wir hier ein Mutabe-Array übergeben bekommen. Dann wäre bei einer einfachen Zuweisung wieder das Problem da, dass man von außen in der Instanz herumfummeln kann.
293
SmartBooks
Objective-C und Cocoa – Band 2
POWER Manche Leute wollen deshalb auch den Getter so gestalten, dass eine entsprechende NSArray-Instanz erzeugt wird. Nur muss man in dieser Richtung bedenken, dass das Ergebnis des Getters nur an ein NSArray, jedoch nicht an ein NSMutableArray zugewiesen werden kann. Der Programmierer müsste also entweder die entsprechende Warnung des Compilers übergehen oder aber zur Laufzeit nachfragen, ob sich hinter dem NSArray-Returnwert nicht doch eine Mutable-Instanz befindet, die er unmittelbar ändern kann. Beides fällt nicht mehr in die Fehlerkategorie »Versehen«, sondern eher in die Kategorie: »höchste kriminelle Energie«. Muss man den Programmierer vor seiner eigenen Kriminalität schützen? … - (void)
insertCards:(NSArray*)value inCardsAtIndexex:(NSIndexSet*)indexes
{ [cards insertObjects:value atIndexes:indexes]; [self setNeedsDisplay:YES]; } - (void)removeCardsAtIndexes:(NSIndexSet*)indexes { [cards removeObjectsAtIndexes:indexes]; [self setNeedsDisplay:YES]; } - (void)replaceCardsAtIndexes:(NSIndexSet*)indexes withCards:(NSArray*)value { [cards replaceObjectsAtIndexes:indexes withObjects:value]; [self setNeedsDisplay:YES]; }
Die weitere Implementierung dürfte sich von selbst erklären. Beachten Sie nur, dass wir hier den View zum Neuzeichnen auffordern.
294
Kapitel 3
View-Controller-Bindung
TIPP In unserer Beispielapplikation können wir davon keinen Gebrauch machen, weil bei uns einzelne Änderungen sich auf den gesamten Viewinhalt auswirken können. In der Regel ist es jedoch möglich: Durch das gezielte Bearbeiten des Arrays lässt sich deutlich einfacher das Rechteck bestimmen, welches neu gezeichnet werden muss. Denken Sie wieder an den Fall der graphischen Applikation, in der Symbole eingefügt werden können. Hier ist klar, dass das umschließende Rechteck gleichzeitig das Redraw-Rechteck ist. Wir haben also nicht nur eine bessere Struktur, sondern auch gleich einen Optimierungsvorteil. Mit diesen Ergänzungen gehen wir in die Implementierung der Observierungsmethode: - (void)observeValueForKeyPath:(NSString*)keyPath ofObject:observed change:(NSDictionary*)change context:(id)context { // Ist es meine Observierung? if( [context isEqualToString:@"cardsBinding"] ) { NSArray* new = [change keyValueChangeNew]; NSIndexSet* indexes = [change keyValueChangeIndexes]; switch( [change keyValueChangeKind] ) { case NSKeyValueChangeSetting: self.cards = new; break; case NSKeyValueChangeInsertion: [self insertCards:new inCardsAtIndexex:indexes]; break; case NSKeyValueChangeRemoval: [self removeCardsAtIndexes:indexes]; break; case NSKeyValueChangeReplacement: [self replaceCardsAtIndexes:indexes withCards:new]; break; }
295
SmartBooks
Objective-C und Cocoa – Band 2
} else { [super observeValueForKeyPath:keyPath ofObject:observed change:change context:context]; } }
Sie sehen, wie einfach das am Ende ist. Das liegt natürlich auch daran, dass uns passgenaue Methoden für das Array zur Verfügung stehen. Diese sind übrigens teilweise erst mit OS X 10.4 eingeführt worden … Ja, wir pfuschen hier noch: Denn bisher wurden ja die Instanzen aus unserem Model in Dictionarys umgewandelt. Hier verlassen wir uns jetzt darauf, dass der Schlüssel im Model dem vom View erwarteten entspricht (suit bzw. value), exportieren also diese Erwartung unter Verstoß gegen die Kapselung. Aber auch dieses Problem werden wir beim Wechsel auf Bindings hinbekommen. Probieren Sie bitte die verschiedenen Actionmethoden im Menü aus.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 58« von der Webseite herunterladen.
Attribute hinter To-many-Relationen Jetzt wird es jedoch noch einmal anstrengend. Den letzten Fall haben wir nämlich noch gar nicht bedacht: Was ist, wenn sich ein Stapel gar nicht ändert, sondern nur eine Karte in ihm? Die Problematik dürfte generell klar sein: Nicht das observierte Array ändert sich, sondern eine Entität, die im Array abgelegt ist. (Das gleiche Problem stellt sich freilich bei Sets.) Wir sind einfach mal so frei, das zu testen. Fügen Sie in AppDelegate.m folgenden Code ein: - (IBAction)changeLastCard:(id)sender { CardStack* stack = [self.stacks objectAtIndex:11]; NSMutableArray* cards = [stack mutableArrayValueForKey:@"cards"]; if( [cards count] > 0 ) { Card* card = [cards lastObject]; card.suit = [NSNumber numberWithUnsignedInteger:2];
296
Kapitel 3
View-Controller-Bindung
} } - (IBAction)setStack:(id)sender
Bitte publizieren Sie die Actionmethode im Header - (IBAction)changeLastCard:(id)sender; @end
und fügen Sie im Inerface Builder wiederum einen Menüeintrag hinzu, den Sie bitte mit der Methode verbinden. Wenn Sie das Programm starten, erzeugen Sie über das Menü eine Karte und ändern diese. Nichts passiert. Jetzt vergrößern Sie bitte das Fenster ein wenig, um ein Neuzeichnen zu erzwingen. Es erscheint das Pik-As. Unsere Actionmethode funktioniert also. Nur wird die Observierung nicht aufgelöst und deshalb kein Neuzeichnen erzwungen. Sie mögen einwenden, dass man doch die Elemente einfach austauschen könnte, wie wir das schon gemacht haben, also die alte Karte entnehmen und eine neue einfügen. Das stimmt aber nicht: Dort wird das Array selbst und nicht die Eigenschaften der gespeicherten Elemente verändert. Abgesehen davon, dass das bei komplizierten Instanzen mühselig ist, können wir das auch nicht durchhalten: Wir können nicht von jedem, der eine Eigenschaft der Entität verändert, verlangen, dass er weiß, in welchem Array er das Element austauschen muss – zumal das Element ja in beliebig vielen Collections liegen kann. Der Sache nach hätten wir den Zustand geschaffen, dass unsere Instanzen unveränderlich sind, weil man sie austauschen muss anstatt sie zu ändern. Book authors
Element name=@"Amin" Element name=@"Christian"
Book authors
Element name=@"Amin" Element name=@"Christian"
Element name=@"Amin"
Book authors
Element name=@"Christian"
Element name=@"Neumann"
Observierung der Collection
Observierung der Collection
Observierung der Collection
Observierung der Eigenschaften
Ein Austausch von Elementen wird von der Observierung der Collection erkannt, die Änderung einer Eigenschaft eines gespeicherten Elementes indessen nur, wenn genau diese observiert wird.
Aber das Problem ist gar keines. Wir wissen ja bereits, wie man einzelne Attribute überwachen kann. Also müssen wir nur diese Observierung und die der Collection verbinden. 297
SmartBooks
Objective-C und Cocoa – Band 2
Und genau hier liegt der Trick: Für jedes Element, welches der Collection hinzugefügt wird, muss eine Observierung eingerichtet werden, für jedes, welches entfernt wird, wieder abgemeldet werden. Grundsätzlich existieren zwei Stellen, an denen man das erledigen kann: Zum einen kann man die Setter dazu verwenden, denn diese sehen ja jede Erweiterung oder Einschränkung. Allerdings müssen Sie beachten, dass unser View nicht zwingend gebunden sein muss. Wir selbst hatten ja vorhin noch eine Data-Source verwendet. Einfacher ist es häufig, die Einrichtung der Observierung gleich in der Observierungsmethode zu machen. Hierfür gibt es dann gleich auch Methoden, die das Ganze bequem gestalten. Implementieren wir das: - (void)removeObserverFromObjects:(NSArray*)items { NSRange oldRange = NSMakeRange( 0, [items count] ); NSIndexSet* indexes; indexes = [NSIndexSet indexSetWithIndexesInRange:oldRange]; [items removeObserver:self fromObjectsAtIndexes:indexes forKeyPath:@"value"]; [items removeObserver:self fromObjectsAtIndexes:indexes forKeyPath:@"suit"]; } …
Dies ist eine reine Hilfsmethode, um eine Observierung zu entfernen. Wir müssen eben wie hier gezeigt die Observierung händisch einrichten und entfernen. Allerdings ist es nicht erforderlich, jedes einzelne Element im Array gesondert zu behandeln. -removeObserver:fromObjectsAtIndexes:forKeyPath: führt das für uns durch, wobei es hier nicht nur um Bequemlichkeit, sondern auch um Verbesserung der Ausführungsgeschwindigkeit geht. Hier wundern Sie sich vielleicht auch über die Art des Zugriffes, nämlich über ein Indexset über alle Elemente des Arrays. Man könnte natürlich auch anhand der Indexe des Change-Dictionarys die Observierungen aus dem eigenen cards-Array entfernen. Bedenken Sie aber bitte, dass bei Cocoa nicht Instanzen in Collections kopiert werden, sondern nur deren Verweise. Es ist also gleichgültig, über welches Array wir die Observierung anmelden. Schauen Sie sich vielleicht noch einmal die Graphik zur Änderung von To-many-Relationships an.
298
Kapitel 3
View-Controller-Bindung
… - (void)addObserverToObjects:(NSArray*)items { NSRange oldRange = NSMakeRange( 0, [items count] ); NSIndexSet* indexes; indexes = [NSIndexSet indexSetWithIndexesInRange:oldRange]; [items
addObserver:self toObjectsAtIndexes:indexes forKeyPath:@"value" options:0 context:@"valueBinding"]; [items addObserver:self toObjectsAtIndexes:indexes forKeyPath:@"suit" options:0 context:@"suitBinding"]; } …
Dies ist das Spiegelbild zur Entfernung und sollte sich schon aus den Methodennamen erklären. Zu beachten ist hier jedoch, dass wir keine Optionen benötigen: Da wir kein eigenes Array mehr in dem View aufbauen, können wir uns die Werte gleich beim Model abholen. Das Problem mit den Schlüsseln lösen wir ja bei den Bindings. … - (void)observeValueForKeyPath:(NSString*)keyPath ofObject:observed change:(NSDictionary*)change context:(id)context { // Ist es meine Observierung? if( [context isEqualToString:@"cardsBinding"] ) { NSArray* new = [change keyValueChangeNew]; NSArray* old = [change keyValueChangeOld]; NSIndexSet* indexes = [change keyValueChangeIndexes]; switch( [change keyValueChangeKind] ) {
299
SmartBooks
Objective-C und Cocoa – Band 2
case NSKeyValueChangeSetting: [self removeObserverFromObjects:old]; self.cards = new; [self addObserverToObjects:new]; break; case NSKeyValueChangeInsertion: [self insertCards:new inCardsAtIndexex:indexes]; [self addObserverToObjects:new]; break; case NSKeyValueChangeRemoval: [self removeObserverFromObjects:old]; [self removeCardsAtIndexes:indexes]; break; case NSKeyValueChangeReplacement: [self removeObserverFromObjects:old]; [self replaceCardsAtIndexes:indexes withCards:new]; [self addObserverToObjects:new]; break; } …
Dies sollte eigentlich verständlich sein, da die Mengenoperationen ja schon besprochen wurden. Auf die entsprechenden Änderungen führen wir also die Hilfsmethoden aus. Wir müssen zudem die neu eingefügten Observierungen abhandeln: … } else if( [context isEqualToString:@"suitBinding"] ) { [self setNeedsDisplay:YES]; } else if( [context isEqualToString:@"valueBinding"] ) { [self setNeedsDisplay:YES]; } else { [super observeValueForKeyPath:keyPath ofObject:observed change:change context:context]; } }
300
Kapitel 3
View-Controller-Bindung
… - (void) dealloc { [self removeObserverFromObjects:self.cards]; … }
Starten Sie jetzt bitte das Programm, fügen Sie eine Karte ein und ändern Sie diese mit dem neuen Menüpunkt. Sollte jetzt klaglos funktionieren.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 59« von der Webseite herunterladen.
Cocoa-Bindings An einigen Stellen hatten wir bereits darauf hingewiesen, dass Key-Value-Observing alleine ein strukturelles Problem birgt, jedenfalls, wenn man es zwischen den MVC-Schichten anwendet. Insgesamt haben wir noch folgende Probleme;
•
Die Einrichtung der Observierung ist von der Observierungsmethode getrennt. Dies betrifft Optionen und Schlüssel.
•
Wir verwenden Schlüssel auf die Modelinstanzen, die wir nicht gewährleisten können. Verstoß gegen den Grundsatz der Kapselung.
•
Wir haben ziemlich mutwillig die eigenen Observierungen des Views im -dealloc entfernt. Tatsächlich muss dies aber geschehen, wenn die Hauptobservierung vom View selbst weggenommen wurde. Das erfuhr der View bloß nie …
•
Bisher holen wir alle Karten eines Stapels ab. Unser Design ist aber so, dass wir lediglich die offenen Karten halten wollen und von den umgedrehten nur die Anzahl.
Bindings und Key-Value-Observing Der erste Schritt besteht darin, die Einrichtung der Observierung und die Observierungsmethode wieder zusammenzuführen. Das beseitigt schon einmal strukturelle Schwierigkeiten, die übrigens immer schlimmer würden. Das Problem ist dann aber, dass der View ja nicht wissen kann, woran er sich binden soll. Ersatzweise bietet er daher eine API an, mit der der Controller wiederum die Observierung in bestimmter Hinsicht steuern kann.
301
SmartBooks
Objective-C und Cocoa – Band 2
Die API wird maßgeblich durch das Protokoll NSKeyValueBindingCreation bestimmt. Neben Methoden, die dem Interface Builder dienen, gibt es für uns vom Code aus nur drei, die relevant sind:
•
-bind:toObject:withKeyPath:options: ist sozusagen der Befehl an den Gebundenen, eine Observierung einzurichten.
• •
unbind: ist der Befehl zur Aufgabe. infoForBinding: liefert die Einstellungen des aktuellen Bindings ab.
Infrastruktur Um also Bindings zu ermöglichen, müssen wir die entprechende Methode implementieren. Es existiert zwar schon eine Implementierung in der Basisklasse NSObject, allerdings taugt die allenfalls für einfache Attribute. Wir haben es zuletzt ja doch zu ziemlich komplexen Observationen gebracht. Fangen wir zunächst damit an, die Binding-Info abzuspeichern. Dazu fügen wir globale Konstanten, eine Instanzvariable und – weil wir hier gerade dabei sind – auch schon Eigenschaften für die zu verwendenden Schlüssel ein: extern NSString* const disclosedCardsBinding; extern NSString* const nonDisclosedCardsCountBinding; @interface CardStackView : NSView { … NSMutableDictionary* bindingInfos; NSString* valueKeyPath; NSString* suitKeyPath; } @property( copy ) NSString* valueKeyPath; @property( copy ) NSString* suitKeyPath; … @end
Die Konstanten dienen dazu, verschiedene Bindings zu unterscheiden. Es können ja pro View mehrere vorhanden sein. Als Identifier bedient man sich einfach eines Strings, der häufig dem Namen der gebundenen Eigenschaft entspricht. Allerdings kann es mehrere Bindings für eine Eigenschaft geben oder mehrere Setter. Denken Sie daran, dass es etwa bei einem Textview als Control mehrere Möglichkeiten gibt, den Wert zu setzen (setFloatValue:, setIntValue: usw.). Es kann hier jedoch nur ein Binding geben, da die Eigenschaft ja synchron sein muss und sich nicht auf verschiedene Quellen gleichzeitig synchronisieren kann. Das Binding wäre dann 302
Kapitel 3
View-Controller-Bindung
dementsprechend value. Ein anderes Beispiel ist das Content-Binding eines Array-Controllers. Zwar kann sich der Array-Controller aus mehreren unterschiedlichen Quellen unterschiedlich befeuern lassen (contentSet, contentArray), jedoch darf stets nur ein Binding gesetzt sein, da sich der Controller nur auf eine logische Eigenschaft content synchronisieren kann. Dies bedeutet also, dass sich der Satz an Bindings, die gleichzeitig gesetzt sein können, an dem Satz der logisch unabhängigen Eigenschaften orientiert, was nicht dem Satz an Accessoren entsprechen muss. Entity value -setIntValue: -setFloatValue: Auch wenn mehrere Accessoren angeboten werden, kann sich dahinter logisch betrachtet nur eine Eigenschaft befinden.
Die beiden weiteren Eigenschaften teilen mit, unter welchem Schlüssel sich die beiden wesentlichen Eigenschaften (Farbe, Wert) der Karten befinden. Eigentlich haben wir ja drei Bindings: einmal die Observierung des Arrays und dann für die beiden Eigenschaften der Karten. Der Tableview löst dieses Problem von Hauptbinding und abhängigen Bindings, indem er ein Content-Binding hat, welches automatisch mit der ersten Spalte gesetzt wird, und dann zwei weitere für die einzelnen Spalten. Dies kann aber Inkonsistenzen zur Folge haben, wenn die Bindings an unterschiedliche Arrays gesetzt werden, was verboten, jedoch möglich ist. Um dies zu vermeiden, implementieren wir hier nur ein Hauptbinding und setzen Schlüsselpfade dran. In -infoForBinding: werden die Einstellungen zu jedem einzelnen Binding abgelegt. Dazu bedienen wir uns eines Dictionarys, dessen Schlüssel der jeweilige Name des Bindings ist. Die einzelnen Einstellungen zu einem Binding – observiertes Objekt, Schlüsselpfad und Optionen – könnten wir ebenfalls in einem Dictionary ablegen. Aber hier droht eine sehr versteckte Falle: Da üblicherweise Controller ihre Views halten, etwa über einen Nib-File, gleichzeitig aber das Dictionary des Views den Controller als observiertes Objekt halten würden, hätte wir einen Retain-Cyle (Band I, S. 246 ff.) Es werden verschiedene Lösungen bis hin zur Benutzung undokumentierter Methoden vorgeschlagen. Man muss sich aber nicht auf solch glattes Eis begeben: Wir erzeugen uns einfach eine Helferklasse, die das observierte Objekt in einer assignProperty hält. Erst auf Anforderung wird darauf ein Dictionary erzeugt, welches sich im Autorelease-Pool befindet und mit ihm entfernt wird. Wird dieses vom Nutzer von -infoForBinding: gespeichert, so übernimmt dieser die Eigentümer303
SmartBooks
Objective-C und Cocoa – Band 2
schaft und ist selbst für die Auflösung des Retainzyklusses verantwortlich. Tatsächlich ist das aber nicht notwendig. (Man kann auch die vorgefertigte Klasse NSMapTable verwenden, die schwache Referenzen kennt. Allerdings erscheint uns der Code dann nicht einfacher.) Machen wir uns an die einfache Implementierung. Erstellen Sie eine neue Klasse BindingInfo aus der Vorlage Objective-C class | NSObject und geben Sie dieser drei Eigenschaften: @interface BindingInfo : NSObject id object; NSString* keyPath; NSDictionary* options; } @property( assign ) id @property( copy ) NSString* @property( copy ) NSDictionary*
{
object; keyPath; options;
+ (BindingInfo*)bindingInfoWithDictionary:(NSDictionary*)info; - (NSDictionary*)dictionary; @end
Die Implementierung ist denkbar einfach: @implementation BindingInfo @synthesize object, keyPath, options; - (NSDictionary*)dictionary { return [NSDictionary dictionaryWithObjectsAndKeys: self.object, NSObservedObjectKey, self.keyPath, NSObservedKeyPathKey, self.options, NSOptionsKey, nil]; } - (id)initWithDictionary:(NSDictionary*)info { self = [super init]; if( self ) { self.object = [info objectForKey:NSObservedObjectKey]; self.keyPath = [info objectForKey:NSObservedKeyPathKey]; self.options = [info objectForKey:NSOptionsKey]; } return self; }
304
Kapitel 3
View-Controller-Bindung
+ (BindingInfo*)bindingInfoWithDictionary:(NSDictionary*)info { return [[[self alloc] initWithDictionary:info] autorelease]; } - (void) dealloc { self.options = nil; self.keyPath = nil; self.object = nil; [super dealloc]; } @end
Zur Bedienung der Eigenschaft implementieren wir drei Methoden. Es sei darauf hingewiesen, dass -infoForBinding: bereits in NSObject vorhanden ist, so dass wir diese Methode nicht publik machen müssen. Die anderen halten wir bewusst privat. Ebenso richten wir private Konstanten für die Nebenbindings ein: #import "KeyValueBindingExtension.h" #import "BindingInfo.h" … NSString* const disclosedCardsBinding = @"disclosedCardsBinding"; NSString* const nonDisclosedCardsCountBinding = @"nonDisclosedCardsCountBinding"; static NSString* const suitBinding = @"suitBinding"; static NSString* const valueBinding = @"valueBinding"; @implementation CardStackView @synthesize alignment, draggedCardsCount; @synthesize valueKeyPath, suitKeyPath; - (void)
setInfo:(NSDictionary*)info forBinding:(NSString*)binding
{ BindingInfo* bindingInfo = [BindingInfo bindingInfoWithDictionary:info]; [bindingInfos setObject:info forKey:binding]; } - (void)removeInfoForBinding:(NSString*)binding
305
SmartBooks
Objective-C und Cocoa – Band 2
{ [bindingInfos removeObjectForKey:binding]; } - (NSDictionary*)infoForBinding:(NSString*)binding { NSDictionary* info = [bindingInfos objectForKey:binding]; if( info ) { return info; } return [super infoForBinding:binding]; } - (NSDictionary*)bindingInfos { return [[bindingInfos copy] autorelease]; } - (void)setBindingInfos:(NSDictionary*)value { if( value != bindingInfos ) { [bindingInfos release]; bindingInfos = [value mutableCopy]; } } … - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { self.bindingInfos = [NSDictionary dictionary]; … } return self; } - (void) dealloc { self.bindingInfos = nil; self.selectedCardsIndices = nil; … }
306
Kapitel 3
View-Controller-Bindung
Bitte beachten Sie auch, dass im -dealloc nicht mehr die selbst angemeldeten Observierungen entfernt werden müssen. Denn auch hier war es strukturell fehlerhaft, da der View die Observierung ja auch nicht veranlasst hatte. Binding einrichten Damit man das Binding einrichten kann, muss die entsprechende Methode hinter der Observierungsmethode implementiert werden: - (void)
bind:(NSString*)binding toObject:(id)observed withKeyPath:(NSString*)keyPath options:(NSDictionary*)bindingOptions
{ …
Der Methodenkopf ist leicht zu verstehen: Letztlich bekommen wir die notwendigen Parameter für eine Einrichtung:
• • • •
binding bezeichnet die gebundene Eigenschaft, siehe oben. observed bezeichnet das Objekt, an das gebunden werden soll, keyPath entsprechend die Eigenschaft. bindingOptions kann verschiedene Optionen enthalten, die Sie bereits aus dem Interface Builder kennen. Sie sind dort zu einem Binding angegeben, wie etwa der Value-Transformer, Nil-Placeholder usw. Es handelt sich nicht um die KVO-Optionen! Das ist ja auch klar: Die Observierungsoptionen wollen wir ja gerade privatisieren, sie dürfen also nicht übergeben werden.
Hier kommt übrigens erneut das Problem mit Hauptbinding und abhängigen Bindings zum Vorschein. Wie Sie sehen, existiert nur ein Parameter keyPath. Wenn Sie aber im Interface Builder an einen Bindings-Controller binden, namentlich an einen Array-Controller, so müssen Sie einen Controller Key und einen Model Key Path eingeben – also zwei Schlüssel. Wie kann das gehen? Der Trick besteht darin, dass der Controller-Key eben nur ein einzelner Schlüssel (daher auch nicht Controller Key Path) ist. Er wird einfach vor den Model-KeyPath gehängt, so dass wir etwa für jede Spalte eines Tableviews Schlüsselpfade wie @"arrangedObjects.suit", @"arrangedObjects.value" usw. bekämen. Für die Observierung wird das dann entsprechend aufgebrochen und der »rohe« Schlüssel für das Array an die Tabelle selbst weitergegeben.
307
SmartBooks
Objective-C und Cocoa – Band 2
Controller Key: items Model KeyPath: name items name Bound rows
Bound items
Row value
Entity name
Row value
Entity name
Aus dem Controller-Key und dem Model-Key-Path wird ein Schlüsselpfad, der in der Bindingsmethode wieder aufgebrochen wird.
In unserer Implementierung wird einfach nur der Controller-Key angegeben und der Pfad zu den einzelnen Eigenschaften als Attribut gesetzt. Dieses System sehen Sie etwa bei dem Tree-Controller für die Children-Beziehung. Wir halten es für übersichtlicher. Außerdem sind so inkonsistente Bindungen ausgeschlossen. Controller Key: items
Row value
Bound rows valueKey
Bound items
Row value
Entity name Entity name
Wir setzen die verschiedenen Schlüsselpfade als Attribute der gebundenen Instanz.
Dementsprechend werden wir jetzt die Observierung einrichten: … if( binding == disclosedCardsBinding ) { if( !bindingOptions ) { bindingOptions = [NSDictionary dictionary];
308
Kapitel 3
View-Controller-Bindung
} NSDictionary* info; info = [NSDictionary dictionaryWithObjectsAndKeys: observed, NSObservedObjectKey, keyPath, NSObservedKeyPathKey, bindingOptions, NSOptionsKey, nil]; [self setInfo:info forBinding:disclosedCardsBinding]; NSKeyValueObservingOptions options; options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial; [observed addObserver:self forKeyPath:keyPath options:options context:binding]; } else { [super bind:binding toObject:observed withKeyPath: keyPath options:bindingOptions]; } }
Zuletzt müssen wir nur noch die Observierungsmethode anpassen: - (void)removeObserverFromObjects:(NSArray*)items { … [items removeObserver:self fromObjectsAtIndexes:indexes forKeyPath:self.valueKeyPath]; [items removeObserver:self fromObjectsAtIndexes:indexes forKeyPath:self.suitKeyPath]; } - (void)addObserverToObjects:(NSArray*)items { …
309
SmartBooks
Objective-C und Cocoa – Band 2
[items
addObserver:self toObjectsAtIndexes:indexes forKeyPath:self.valueKeyPath options:0 context:valueBinding]; [items addObserver:self toObjectsAtIndexes:indexes forKeyPath:self.suitKeyPath options:0 context:suitBinding]; }
Hier passen wir also nur die Schlüsselpfade an, damit dies im Model frei gewählt werden kann. Außerdem verwenden wir die Konstanten jetzt auch in der Observierungsmethode: - (void)observeValueForKeyPath:(NSString*)keyPath ofObject:observed change:(NSDictionary*)change context:(id)context { // Ist es meine Observierung? if( [context isEqualToString:disclosedCardsBinding] ) { … } else if( [context isEqualToString:suitBinding] ) { [self setNeedsDisplay:YES]; } else if( [context isEqualToString:valueBinding] ) { [self setNeedsDisplay:YES]; … }
Jetzt müssen wir nur noch im AppDelegate das Binding einrichten: - (void)awakeFromNib { CardStack* targetStack = [self.stacks objectAtIndex:targetStackIndex]; CardStackView* targetView = [tableBaizeView viewWithStackIndex:targetStackIndex]; targetView.valueKeyPath = @"value";
310
Kapitel 3
View-Controller-Bindung
targetView.suitKeyPath = @"suit"; [targetView bind:disclosedCardsBinding toObject:targetStack withKeyPath:@"cards" options:nil]; }
Das sollte jetzt wieder funktionieren. Bitte probieren Sie es wieder über das Menü aus. Wir können aufgrund der sauberen API der Bindings jetzt sogar wieder unsere Subviews verheimlichen. Dazu implementieren wir einfach wieder ein vorderes API im TableBaizeView und lassen diesen die Bindingseinrichtung an seine Subviews durchreichen: extern NSString* const targetDisclosedCardsBinding; @interface TableBaizeView : NSView { … } - (void)setValueKeyPath:(NSString*)valueKeyPath andSuitKeyPath:(NSString*)suitKeyPath;
In der Implementierung: NSString* const targetDisclosedCardsBinding = @"targetDisclosedCardsBinding"; @implementation TableBaizeView … - (void)setValueKeyPath:(NSString*)valueKeyPath andSuitKeyPath:(NSString*)suitKeyPath { for( CardStackView* stackView in [self subviews] ) { stackView.valueKeyPath = valueKeyPath; stackView.suitKeyPath = suitKeyPath; } } - (void)
bind:(NSString*)binding toObject:(id)observed withKeyPath:(NSString*)keyPath options:(NSDictionary*)options
311
SmartBooks
Objective-C und Cocoa – Band 2
{ CardStackView* stackView; if( binding == targetDisclosedCardsBinding ) { stackView = [self viewWithStackIndex:targetStackIndex]; [stackView bind:disclosedCardsBinding toObject:observed withKeyPath:keyPath options:options]; } else { [super bind:binding toObject:observed withKeyPath: keyPath options:options]; } } - (id)initWithFrame:(NSRect)frame
AUFGEPASST Der Ehrlichkeit halber müssen wir zugeben, dass bei einer nachträglichen Änderung der Schlüsselpfade (suitKeyPath, valueKeyPath) alle existierenden Observierungen der enthaltenen Elemente abgemeldet und mit den neuen Schlüsseln wieder angemeldet werden müssen. Wir sparen uns das hier, weil wir es nicht benötigen. Entsprechend die Änderung in AppDelegate.m: - (void)awakeFromNib { [tableBaizeView setValueKeyPath:@"value" andSuitKeyPath:@"suit"]; CardStack* targetStack = [self.stacks objectAtIndex:11]; [tableBaizeView bind:targetDisclosedCardsBinding toObject:targetStack withKeyPath:@"cards" options:nil]; }
Binding abmelden Das Problem der lose herumfliegenden Observierung bekommen wir damit auch gelöst. Wir können entsprechend das Binding wieder abmelden. Dazu dient die 312
Kapitel 3
View-Controller-Bindung
Methode -unbind. Implementieren wir diese zunächst im CardStackView nach der Methode -bind:toObject:withKeyPath:options: - (void)unbind:(NSString*)binding { if( binding == disclosedCardsBinding ) { // Abhaengige Observierungen loeschen. [self removeObserverFromObjects:cards]; NSDictionary* info = [self infoForBinding:disclosedCardsBinding]; id observed = [info objectForKey:NSObservedObjectKey]; NSString* keyPath = [info objectForKey:NSObservedKeyPathKey]; [observed removeObserver:self forKeyPath:keyPath]; } else { [super unbind:binding]; } }
Auch hier exportieren wir das wieder über den TableBaizeView an der gleichen Stelle: - (void)unbind:(NSString*)binding { CardStackView* stackView; if( binding == targetDisclosedCardsBinding ) { stackView = [self viewWithStackIndex:targetStackIndex]; [stackView unbind:disclosedCardsBinding]; } else { [super unbind:binding]; } }
Im AppDelegate können wir das Binding dann ordnungsgemäß abmelden und den entsprechenden Code in -dealloc entfernen: - (void)applicationWillTerminate:(NSNotification*)notification { [tableBaizeView unbind:targetDisclosedCardsBinding]; } - (void)dealloc { self.stacks = nil; [super dealloc]; }
313
SmartBooks
Objective-C und Cocoa – Band 2
Wir hängen uns also diesmal in die Methode zum Beenden der Applikation. Beim Application-Delegate wird nämlich -dealloc nicht zwingend (praktisch nie) aufgerufen, weil ohnehin das System den Speicher »aufräumt«. Damit Sie mal die Löschung des Bindings im Debugger sehen können, haben wir das vorverschoben.
AUFGEPASST Da wir jetzt die Klasse CardStackView wieder vollständig hinter der Klasse TableBaizeView versteckt haben, können Sie in AppDelegate.m auch den Import von CardStackView.h herausnehmen. Zum Beleg der Aussage übersetzt der Compiler das Projekt einwandfrei. Testen Sie ein wenig mit dem Programm. Die entsprechenden Änderungen des Stapels sollten funktionieren.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 60« von der Webseite herunterladen. Mehrfachbindings Ihnen ist vielleicht schon aufgefallen, dass sich manche Bindings im Interface Builder vervielfältigen, sobald ein Binding gesetzt wird. Das Enabled-Binding für Buttons ist etwa so ein Fall: Sobald wir das Binding Enabled setzen, erscheint automatisch ein neues Binding Enabled2, dann Enabled3 usw. Es dürfte klar sein, wie das geht: Da ja unsere Binding-API nicht irgendein Zauberwerk ist, welches im Hintergrund unkontrollierbar arbeitet, sondern lebender, von uns zu beeinflussender Code, können wir so etwas problemlos implementieren. Wir müssen halt nur in der Bindingsmethode entsprechend auf einen Bindingnamen zuzüglich eines Indexes abfragen und das Ergebnis unserer Synchronisierungen mit »Und« verknüpfen (oder »Oder«, wenn Sie das gerade benötigen). Der Knackpunkt: Die Anzahl und die Namen der angebotenen Bindings lassen sich sogar noch zur Laufzeit beeinflussen. Bei uns geht es indessen nicht um die logische Verknüpfung, sondern um einzelne Stapel. Das ändert an der Methodik jedoch nicht.
AUFGEPASST Eine andere – vermutlich die nächstliegende – Möglichkeit wäre es, ein Binding für Stacks selbst anzubieten. Wir würden also eine weitere Observierungsebene hinzufügen. Abgesehen davon, dass Sie dabei nichts Neues lernen würden, ist das hier aber auch gar nicht notwendig: Die Struktur des Spiels und damit der Stapel an sich ändert sich ja nicht. Wozu sollte man also etwas synchronisieren, was sich nicht ändert? 314
Kapitel 3
View-Controller-Bindung
Zunächst bieten wir in TableBaizeView.m auch für die anderen Eigenschaften ein Binding an, aber für die Spielerstapel nicht jeweils zehn, sondern nur eines: extern NSString* const targetDisclosedCardsBinding; extern NSString* const depotDisclosedCardsBinding; extern NSString* const playerNDisclosedCardsBinding;
Und dann die Implementierung: NSString* const targetDisclosedCardsBinding = @"targetDisclosedCardsBinding"; NSString* const depotDisclosedCardsBinding = @"depotDisclosedCardsBinding"; NSString* const playerNDisclosedCardsBinding = @"playersDisclosedCardsBinding"; …
Sie sehen schon an den Konstanten, wohin die Reise geht: Damit wir jetzt nicht zehn Bindings alleine für die Stapel einrichten müssen, benutzen wir einen kleinen Trick: Das Binding dient nur als Grundstock. Hinten hängen wir den Index dran. Implementieren wir, damit das klarer wird: - (void)
bind:(NSString*)binding toObject:(id)observed withKeyPath:(NSString*)keyPath options:(NSDictionary*)options
{ CardStackView* stackView; if( binding == targetDisclosedCardsBinding ) { … } else if( binding == depotDisclosedCardsBinding ) { stackView = [self viewWithStackIndex:depotStackIndex]; [stackView bind:disclosedCardsBinding toObject:observed withKeyPath:keyPath options:options]; } else if( [binding hasPrefix:playerNDisclosedCardsBinding] ){ NSUInteger indexPosition; indexPosition = [playerNDisclosedCardsBinding length]; NSUInteger stackIndex = [[binding substringFromIndex:indexPosition] intValue];
315
SmartBooks
Objective-C und Cocoa – Band 2
CardStackView* stackView; stackView = [self viewWithStackIndex:stackIndex]; [stackView bind:disclosedCardsBinding toObject:observed withKeyPath:keyPath options:options]; } else { … } }
Sie sehen also, dass wir aus dem Binding des Index für den Stackview ziehen und dann nur dieses eine Stackview binden. Das Unbinding ändern wir entsprechend: - (void)unbind:(NSString*)binding { CardStackView* stackView; if( binding == targetDisclosedCardsBinding ) { … } else if( binding == depotDisclosedCardsBinding ) { stackView = [self viewWithStackIndex:depotStackIndex]; [stackView unbind:disclosedCardsBinding]; } else if( [binding hasPrefix:playerNDisclosedCardsBinding] ){ NSUInteger indexPosition; indexPosition = [playerNDisclosedCardsBinding length]; NSUInteger stackIndex = [[binding substringFromIndex:indexPosition] intValue]; CardStackView* stackView; stackView = [self viewWithStackIndex:stackIndex]; [stackView unbind:disclosedCardsBinding]; } else { [super unbind:binding]; } }
Dementsprechend müssen wir in AppDelegate.m jeden Spielerstapel einzeln bedienen. Wir hängen dabei an die Konstante den ensprechenden Index an.
316
Kapitel 3
View-Controller-Bindung
- (void)awakeFromNib { … CardStack* depotStack = [self.stacks objectAtIndex:depotStackIndex]; [tableBaizeView bind:depotDisclosedCardsBinding toObject:depotStack withKeyPath:@"cards" options:nil]; NSUInteger index; for( index = 0; index < playerStacksCount; index++ ) { NSUInteger stackIndex = index + playerStacksMinIndex; NSString* binding = [NSString stringWithFormat:@"%@%d", playerNDisclosedCardsBinding, stackIndex]; CardStack* stack = [self.stacks objectAtIndex:stackIndex]; [tableBaizeView bind:binding toObject:stack withKeyPath:@"cards" options:nil]; } } - (void)applicationWillTerminate:(NSNotification*)notification { [tableBaizeView unbind:targetDisclosedCardsBinding]; [tableBaizeView unbind:depotDisclosedCardsBinding]; NSUInteger stackIndex; NSUInteger index; for( index = 0; index < playerStacksCount; index++ ) { stackIndex = index + playerStacksMinIndex; NSString* binding; binding = [NSString stringWithFormat:@"%@%d", playersNDisclosedCardsBinding, stackIndex]; [tableBaizeView unbind:binding]; } }
317
SmartBooks
Objective-C und Cocoa – Band 2
AUFGEPASST In der finalen Version benötigen wir freilich kein Binding für die aufgedeckten Karten im Depotstapel. Die sind ja alle zugedeckt. Wir behandeln dies hier aber analog zu allen anderen. Sie werden jetzt nach einem Programmstart sehen, dass die entsprechenden Stapel alle erscheinen. Natürlich alle aufdeckt, denn die unterschiedliche Struktur von Model (alle Karten auf einem Stapel) und Views (nur die offenen Karten gespeichert) ist hier noch nicht ausgeglichen. Aber immerhin funktioniert der eigentliche Datenaustausch jetzt über Bindings. Wie gesagt: Es ging darum zu zeigen, dass das Interface über die Bindingmethode flexibel und normaler Bestandteil Ihres Codes ist. Es werkelt da kein heimlicher Hintergrundmechanismus.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 61« von der Webseite herunterladen.
Binding-Optionen (Value-Transformer) Eine wichtige Aufgabe der Data-Source (diese ersetzen wir ja durch die Bindings) war es, Umwandlungen vorzunehmen. Wir haben dies teilweise heimlich schon gemacht, weil in unseren Karten ja Integer auftauchten, die über das Key-ValueCoding automatisch zu NSNumber-Instanzen wurden. Aber wir haben ja größere Probleme, nämlich insbesondere den Kartenhaufen im Depot unten links und die zugedeckten Karten. Wir hatten bei Data-Sourcen dieses Problem gelöst, indem wir an dem Protokoll das Modell in seine einzelnen Eigenschaften zerlegten, über die Methoden transferierten und dann auf der anderen Seite wieder zusammensetzten. Das funktioniert hier jedoch nicht. Es gibt aber die Möglichkeit, sich in die Observierungsleitung hineinzudrängeln. Dazu können wir wie auch im Interface Builder einen ValueTransformer verwenden. Dieser hat die Aufgabe, einen Eingangswert in einen Ausgangswert umzurechnen. Ist es ein sogenannter Reverse-Value-Transformer, dann geht das auch in die andere Richtung, was wir allerdings nicht benötigen. Value-Transformer programmieren Wir machen uns einen Value-Transformer, der die Karten im Depot wieder auf ein Zehntel reduziert.
318
Kapitel 3
View-Controller-Bindung
Zunächst müssen wir uns eine Klasse schreiben, die die entsprechende Funktionalität anbietet. Hierzu erzeugen Sie sich eine neue Klasse (Objective-C class | NSObject) und nennen diese CompressArrayValueTransformer. Im Header ändern Sie die Basisklasse und fügen eine Property hinzu: @interface CompressArrayValueTransformer : NSValueTransformer { NSUInteger compression; } @property NSUInteger compression; @end
Die Arbeit erfolgt in der Implementierung: @implementation ArrayCompressionValueTransformer @synthesize compression; + (Class)transformedValueClass { return [NSArray class]; } …
Hier geben wir an, welche Klasse geliefert wird. Der Rückgabewert ist ein Klassenobjekt, weshalb er auf Class zu typisieren ist. … + (BOOL)allowsReverseTransformation { return NO; } …
Wir verbieten die Umkehroperation. Würden wir einen Value-Transformer implementieren, der dies beherrscht, so müsste YES zurückgegeben werden. … - (id)transformedValue:(id)value { // Wir arbeiten aus Prinzip nur mit Arrays zusammen if( ![value isKindOfClass:[NSArray class]] ) { return nil; }
319
SmartBooks
Objective-C und Cocoa – Band 2
NSMutableArray* transformedValue; transformedValue = [NSMutableArray array]; NSUInteger index; NSUInteger step = self.compression; for( index = 0; index < [value count]; index += step ) { [transformedValue addObject:[value objectAtIndex:index]]; } return transformedValue; } @end
Schließlich die eigentliche Umwandlungsmethode, die -transformedValue heißen muss. Die Methode für die Umkehrfunktion hieße -reverseTransformedValue. Value-Transformer anmelden In AppDelegate.m erzeugen wir dann zwei Instanzen von diesem Value-Transformer und übergeben sie als Option den Bindings. #import "CompressArrayValueTransformer.h" @implementation AppDelegate … - (void)awakeFromNib { … CompressArrayValueTransformer* transformer; transformer = [[[CompressArrayValueTransformer alloc] init] autorelease]; transformer.compression = 3; NSDictionary* options = [NSDictionary dictionaryWithObject:transformer forKey:NSValueTransformerBindingOption]; CardStack* targetStack = [self.stacks objectAtIndex:targetStackIndex]; [tableBaizeView bind:targetDisclosedCardsBinding toObject:targetStack withKeyPath:@"cards" options:options]; …
320
Kapitel 3
View-Controller-Bindung
Hier wird also eine Instanz unserer Klasse erzeugt, parametrisiert und als Option übergeben. Wie Sie sehen, wenden wir den Transformer zunächst auf den Target stack mit der Compression 3 an. Die Verwendung der 3 anstelle einer vielleicht erwarteten 13 für einen kompletten Satz Karten dient hier nur dazu, dass Sie das Verhalten gleich über das Menü besser testen können.
AUFGEPASST Wenn Sie den Value-Transformer auch im Interface Builder nutzen wollen, dann muss er im System mittels der Methode +setValueTransformer:forName: (NSValueTransformer) angemeldet werden. Als Ort hierfür schlägt Apple +initialize (AppDelegate) vor. Entscheidend ist, dass die Anmeldung vor der Benutzung erfolgt. Sie können dabei für verschiedene Instanzen unterschiedliche Namen wählen. Übergeben wird dann dem Binding nicht die Instanz, sondern der Name, und das gebundene Objekt muss sich den Value-Transformer mittels +valueTransformerForName: holen. Die entsprechende Binding-Option für den Namen eines Value-Transformers heißt NSValueTransformerNameBindingOption. Entsprechend unterstützen wir den Value-Transformer in unserem Binding. Dazu muss in der Observierungsmethode nur der erhaltene Wert an den Value-Transformer übergeben und dann das Ergebnis weiterverwendet werden. Halt! Da wir eine To-many-Observierung hatten, erhalten wir ja unter Umständen nur die Änderung, also etwa eine einzelne Einfügung. Das hilft uns nicht. Wir müssen also bei der Verwendung eines Value-Transformers das gesamte Array betrachten, an dem wir hängen. Womit wir beim nächsten Problem wären: Wir bekommen ja nur einen Teil in dem change-Dictionary geliefert. Aber wir können das komplette Array ermitteln, weil wir das observierte Objekt und den observierten Pfad kennen. Den letzten Stand vor der Änderung sehen wir an unserer gebundenen Eigenschaft. Übrigens würden wir im weiterenVerlauf mit diesem Dictionary ohnehin Probleme bekommen. Darauf kommen wir noch zurück. Daher implementieren wir insgesamt die Observierungsmethode aus den bekannten Eigenschaften: - (void)observeValueForKeyPath:(NSString*)keyPath ofObject:observed change:(NSDictionary*)change context:(id)context { // Ist es meine Observierung? if( context == disclosedCardsBinding ) {
321
SmartBooks
Objective-C und Cocoa – Band 2
NSDictionary* info; info = [self infoForBinding:disclosedCardsBinding]; NSDictionary* options; options = [info objectForKey:NSOptionsKey]; NSArray* new = [observed valueForKeyPath:keyPath]; NSValueTransformer* transformer = [options objectForKey:NSValueTransformerBindingOption]; if( transformer ) { new = [transformer transformedValue:new]; } NSArray* old = self.cards; …
Die zentrale Erkenntnis, die Sie hieraus ziehen sollten, ist es, dass die Umwandlung durch den Value-Transformer nicht automatisch vom Key-Value-Observing vorgenommen wird, sondern wir dies explizit machen müssen. Das hat jedoch den Vorteil, dass wir uns unterschiedlichste Optionen ausdenken können. Sie werden ja durch unseren Code abgearbeitet. Allerdings erwächst daraus auch das Problem, dass bei einem Value-Transofrmer in einer To-many-Relationship die Einträge für die neuen und alten Instanzen nicht mehr gültig sind. Überlegen Sie sich zur Kontrolle den Fall, dass eine Karte eingefügt wird. Dies würde uns im New-Key übergeben werden und auch den Val ue-Transformer durchwandern. Wird jetzt eine weitere, also zweite, Karte hinzugefügt, so wird uns Key-Value-Observing diese wieder als neue Instanz melden. Der Value-Transformer veschluckt diese aber wie gewünscht und liefert ein unverändertes Array. Handeln wir jetzt also wie bisher eine Insertion ab, würden wir eine Instanz in das cards-Array des Views einfügen, die da nicht hingehört. Die Schlüssel des Key-Val ue-Observers liegen sozusagen vor dem Transformer. Wir können sie daher nicht verwenden. Das führt zu zwei notwendigen Änderungen: Zum einen holen wir uns alte und neue Elemente jetzt über die lokale Eigenschaft cards des Views (alt) bzw. über den gespeicherten Schlüsselpfad (neu) ab. Außerdem werten wir nicht mehr den Change-Kind aus, sondern setzen das Array komplett: … [self removeObserverFromObjects:old]; self.cards = new; [self addObserverToObjects:new]; } else if( context == suitBinding
322
) {
Kapitel 3
View-Controller-Bindung
… } else { [super observeValueForKeyPath:keyPath ofObject:observed change:change context:context]; } }
HILFE Sie können natürlich wie bei der Implementierung von Key-Value-Observing für Mutable-Sets selbst die Differenz ermitteln. Das erfolgt ja wiederum in dem Value-Transformer. Wenn Sie jetzt die Anwendung starten, werden Sie sehen, dass nur jede dritte Karte, die Sie im Menü mit dem Eintrag für -addCards: einfügen, erscheint.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 63« von der Webseite herunterladen. Sie haben hier also gesehen, wie man Value-Transformer erzeugen und einbinden kann. Entsprechendes gilt auch für die anderen Optionen, die ein Binding anbietet. Letztlich sind Ihnen da keine Grenzen gesetzt, und Sie können selbst Optionen angeben. So hätten wir etwa die Schlüssel für Farbe und Wert auch als Option implementieren können. Der Unterschied hätte dann darin gelegen, dass dies pro Binding einstellbar ist. Braucht man also eher nicht.
Bindings-Controller Ein weiteres wichtiges Element von Cocoa-Bindings sind die Controller. Die Anwendung kennen Sie ja bereits aus dem ersten Band. Bisher haben wir sie links liegen lassen, da wir so den unmittelbaren Zusammenhang zwischen observierter und gebundener Instanz sehen konnten. Aber natürlich bieten Bindings-Controller auch Vorteile. Zu nennen wären hier: Sortierung über Sort-Descriptoren, Selektionsmanagement und Filterung über Filter-Prädikate. Nutzung eines Array-Controllers Diese Filter-Prädikate wollen wir benutzen, um die zugedeckten Karten aus dem Array zu entfernen. Denn zugedeckt bedeutet ja nichts anderes, als dass die Eigenschaft disclosed einer Karte den Wert NO hat. 323
SmartBooks
Card Herz As zugedeckt Card
Pik 4 offen
Objective-C und Cocoa – Band 2
CardStack cards …
Array-Controller contentArray arrangedObjects filterPredicate
CardStackView cards …
Filter-Predicate disclosed=YES
Card Karo 8 offen
Card Pik 4 offen Card Karo 8 offen
Der Array-Controller wird in unser Binding gelegt und filtert die zugedeckten Karten.
Für jeden Stapel erzeugen wir einen Array-Controller. Diesen parametrisieren wir mit dem Filterprädikat und legen ihn in die Bindung. Zunächst eine Eigenschaft definieren: @interface AppDelegate : NSObject { NSArray* stacks; NSArray* filterControllers; IBOutlet TableBaizeView* tableBaizeView; } @property( copy ) NSArray* stacks; @property( copy ) NSArray* filterControllers;
und in der Implementierung: @synthesize stacks; @synthesize filterControllers;
Diese Controller setzen wir dann in unser Binding, indem wir das View nicht mehr unmittelbar an das Model binden, sondern den View an den Array-Controller und den Array-Controller an das Model. - (void)awakeFromNib { … NSUInteger index; NSMutableArray* controllers = [NSMutableArray array]; NSPredicate* filter;
324
Kapitel 3
View-Controller-Bindung
filter = [NSPredicate predicateWithFormat:@"disclosed==YES"]; for( index = 0; index < playerStacksCount; index++ ) { stackIndex = index + playerStacksMinIndex; // Controller ... NSArrayController* controller; controller =[[[NSArrayController alloc] init] autorelease]; [controllers addObject:controller]; [controller setFilterPredicate:filter]; // ... und dessen Binding an das Model CardStack* stack = [self.stacks objectAtIndex:stackIndex]; [controller bind:NSContentArrayBinding toObject:stack withKeyPath:@"cards" options:nil]; // Bindung des Views an den Controller NSString* binding = [NSString stringWithFormat:@"%@%d", playersDisclosedCardsBinding, stackIndex]; [tableBaizeView bind:binding toObject:controller withKeyPath:@"arrangedObjects" options:nil]; } self.filterControllers = controllers; }
Bitte geben Sie die Controllers-Property im -dealloc auch wieder frei. Sie sehen jetzt nur noch die offenen Karten auf dem Schirm. TableBaizeView
CardStack cards
NSArrayController contentArray arrangedObjects
CardStackView cards
playersDisclosedCardsBinding0
CardStack cards
NSArrayController contentArray arrangedObjects
CardStackView cards
playersDisclosedCardsBinding1
Aus einem Binding machen wir mehrere, indem wir einen Index anhängen.
325
SmartBooks
Objective-C und Cocoa – Band 2
Der Einsatz eines Array-Controllers hat übrigens einen riesigen Vorteil: Um die offenen Karten zu bekommen, muss nicht nur das Array der Karten selbst observiert werden, sondern darin auch der Schlüssel disclosed. Es ist also wieder eine komplette Durchobservierung notwendig, wie wir sie oben für Farbe und Wert vorgenommen haben. Der Array-Controller macht dies automatisch für uns, wenn wir es ihm nur sagen. Das erspart doch einiges an Arbeit, wie Sie ja zwischenzeitlich wissen. Sie können das auch einmal ausprobieren. Wir schreiben uns eine neue Actionmethode im AppDelegate und schalten diese Option ein: @interface AppDelegate : NSObject { … - (IBAction)discloseCards:(id)sender; @end
Und in AppDeleagte.m: - (IBAction)discloseCards:(id)sender { CardStack* stack; stack = [self.stacks objectAtIndex:playerStacksMinIndex]; for( Card* card in stack.cards ) { card.disclosed = YES; } } - (void)awakeFromNib { … for( index = 0; index < playerStacksCount; index++ ) { … [controllers addObject:controller]; [controller setAutomaticallyRearrangesObjects:YES]; … } … }
Wenn Sie sich jetzt einen entsprechenden Menüpunkt erstellen, der die Actionmethode ausführt, sehen Sie, dass die Karten des ersten Stapels sichtbar werden. Der Array-Controller hat also bemerkt, dass sich die im Filterprädikat genannte Eigenschaft geändert hat, weil er selbst diese observiert.
326
Kapitel 3
View-Controller-Bindung
Eigene Eigenschaft in Array-Controller-Klasse Card disclosed=NO Card disclosed=NO
NSArrayController contentArray arrangedObjects (Filter) filterdOutCount=3
CardStackView cards countOfNonDisclosedCards
Card disclosed=NO Card disclosed=YES Card disclosed=YES
Wir ergänzen den Array-Controller um eine Eigenschaft, die die Anzahl der herausgefilterten (zugedeckten) Karten enthält.
Natürlich ist es kein Zustand, dass die zugedeckten Karten ganz verschwinden. Wir können aber die Sache lösen, indem wir eine Subklasse von NSArrayController erstellen, die die Eigenschaft nonDisclosedCards erzeugt. Erstellen Sie sich eine neue Klasse von der Vorlage Objective-C class | NSObject und nennen Sie diese DisclosedFilterArrayController. Im Header fügen wir die neue Eigenschaft hinzu: @interface DisclosedFilterArrayController : NSArrayController{ NSUInteger filteredOutCount; } @property NSUInteger filteredOutCount; @end
Die wichtigste Methode in einer Subklasse von NSArrayController ist -arrangeObjects:, durch die alle Objekte zwecks Filterung und Sortierung laufen. Da dies inzwischen Standardfunktionlität ist, muss man nur noch selten NSArrayController ableiten. Wir ziehen hieraus aber einen Vorteil: @implementation DisclosedFilterArrayController @synthesize filteredOutCount; - (NSArray*)arrangeObjects:(NSArray*)objects { NSLog( @"new %@", objects );
327
SmartBooks
Objective-C und Cocoa – Band 2
NSUInteger unfilteredCount = [objects count]; objects = [super arrangeObjects:objects]; NSLog( @"filtered %@", objects ); NSUInteger filteredCount = [objects count]; self.filteredOutCount = unfilteredCount - filteredCount; NSLog( @"nonDisclosed: %d", self.filteredOutCount ); return objects; } @end
Diesen eigenen Array-Controller müssen wir nur noch bei der Anmeldung der Bindings im Application-Delegate benutzen: #import "CompressArrayValueTransformer.h" #import "DisclosedFilterArrayController.h" … - (void)awakeFromNib { … for( index = 0; index < playerStacksCount; index++ ) { stackIndex = index + playerStacksMinIndex; // Controller ... NSArrayController* controller; controller =[[[DisclosedFilterArrayController alloc] init] autorelease]; [controllers addObject:controller]; … } … }
Ändern wir die Actionmethode so, dass nur eine Karte aufgedeckt wird: - (IBAction)discloseCards:(id)sender { CardStack* stack; stack = [self.stacks objectAtIndex:playerStacksMinIndex]; NSEnumerator* cardsEnum = [stack.cards reverseObjectEnumerator];
328
Kapitel 3
View-Controller-Bindung
for( Card* card in cardsEnum ) { if( card.disclosed == NO ) { card.disclosed = YES; break; } } }
Wenn Sie die neue Actionmethode benutzen, sollten Sie aber auch eine Besonderheit beachten. Zum Beispiel das Log für das erste Aufdecken einer Karte: >… new ( ♡12+ ) >… filtered ( ♡12+ ) >… nonDisclosed: 0 >… new ( ♡12+, ♠2-, ♡13-, ♠10-, ♠7-, ♠10+ ) >… filtered ( ♡12+, ♠10+ )
Sie sehen, dass die Methode zunächst nur das veränderte Element enthält. Vermutlich wird der Array-Controller hiermit probieren, ob die Änderung überhaupt relevant ist. Es kann bei gesetztem Filter ja sein, dass bereits das neu eingefügte Element herausfällt, so dass überhaupt keine zeitraubende Überprüfung des gesamten Contents erforderlich ist. Sie können mit einem Hack dies vermeiden, indem Sie die Anzahl der Objekte, die über die Parameterliste kommen, mit der Anzahl der Objekte vergleichen, die sich hinter der content-Eigenschaft des Array-Controllers befinden. Nur bei Gleichheit handelt es sich um den finalen Test. Nehmen wir außerdem die Logs heraus:
329
SmartBooks
Objective-C und Cocoa – Band 2
- (NSArray*)arrangeObjects:(NSArray*)objects { if( [self.content count] == [objects count] ) { NSUInteger unfilteredCount = [objects count]; objects = [super arrangeObjects:objects]; NSUInteger filteredCount = [objects count]; self.filteredOutCount = unfilteredCount - filteredCount; return objects; } return [super arrangeObjects:objects]; }
Da unser Array-Controller jetzt eine entsprechende Eigenschaft hat, können wir in unseren Views entsprechende Bindings erstellen. Fangen wir mit dem CardStackView an: - (void)
bind:(NSString*)binding toObject:(id)observed withKeyPath:(NSString*)keyPath options:(NSDictionary*)bindingOptions
{ if( binding == disclosedCardsBinding ) { … } else if( [ binding isEqualToString:nonDisclosedCardsCountBinding] ) { if( !bindingOptions ) { bindingOptions = [NSDictionary dictionary]; } NSDictionary* info; info = [NSDictionary dictionaryWithObjectsAndKeys: observed, NSObservedObjectKey, keyPath, NSObservedKeyPathKey, bindingOptions, NSOptionsKey, nil]; [self setInfo:info forBinding:nonDisclosedCardsCountBinding]; NSKeyValueObservingOptions options; options = NSKeyValueObservingOptionNew
330
Kapitel 3
View-Controller-Bindung
| NSKeyValueObservingOptionInitial; [observed addObserver:self forKeyPath:keyPath options:options context:nonDisclosedCardsCountBinding]; } else { … } } - (void)unbind:(NSString*)binding { if( binding == disclosedCardsBinding ) { … } else if( binding == nonDisclosedCardsCountBinding ) { NSDictionary* info = [self infoForBinding:disclosedCardsBinding]; id observed = [info objectForKey:NSObservedObjectKey]; NSString* keyPath = [info objectForKey:NSObservedKeyPathKey]; [observed removeObserver:self forKeyPath:keyPath]; [self removeInfoForBinding:binding]; } else { [super unbind:binding]; } }
Die dazugehörige Observierungsroutine: - (void)observeValueForKeyPath:(NSString*)keyPath ofObject:observed change:(NSDictionary*)change context:(id)context { // Ist es meine Observierung? if( context == disclosedCardsBinding ) { … } else if( context = valueBinding ) { …
331
SmartBooks
Objective-C und Cocoa – Band 2
} else if( context = suitBinding ) { … } else if( context == nonDisclosedCardsCountBinding ) { NSDictionary* info; info = [self infoForBinding:nonDisclosedCardsCountBinding]; NSNumber* new = [observed valueForKeyPath:keyPath]; self.countOfNonDisclosedCards = [new unsignedIntegerValue]; } else { … } }
Machen wir eine API dafür im TableBaizeView: extern NSString* const playersDisclosedCardsBinding; extern NSString* const playersNonDisclosedCardsCountBinding; @interface TableBaizeView : NSView {
die wir implementieren: NSString* const playerNDisclosedCardsBinding = @"playerNDisclosedCardsBinding"; NSString* const playersNonDisclosedCardsCountBinding = @"playersNonDisclosedCardsCountBinding"; … - (void) bind:(NSString*)binding toObject:(id)observed withKeyPath:(NSString*)keyPath options:(NSDictionary*)options { CardStackView* stackView; if( binding == targetDisclosedCardsBinding ) { … } else if( binding == depotDisclosedCardsBinding ) { … } else if( [binding hasPrefix:playersDisclosedCardsBinding] ){ … } else if( [binding hasPrefix:playersNonDisclosedCardsCountBinding] ){
332
Kapitel 3
View-Controller-Bindung
NSUInteger indexPosition; indexPosition = [playersNonDisclosedCardsCountBinding length]; NSUInteger stackIndex = [[binding substringFromIndex:indexPosition] intValue]; CardStackView* stackView; stackView = [self viewWithStackIndex:stackIndex]; [stackView bind:nonDisclosedCardsCountBinding toObject:observed withKeyPath:keyPath options:options]; } else { … } } - (void)unbind:(NSString*)binding { CardStackView* stackView; if( binding == targetDisclosedCardsBinding ) { … } else if( binding == depotDisclosedCardsBinding ) { … } else if( [binding hasPrefix:playersDisclosedCardsBinding] ){ … } else if( [binding hasPrefix:playersNonDisclosedCardsCountBinding] ){ NSUInteger indexPosition; indexPosition = [playersNonDisclosedCardsCountBinding length]; NSUInteger stackIndex = [[binding substringFromIndex:indexPosition] intValue]; CardStackView* stackView; stackView = [self viewWithStackIndex:stackIndex]; [stackView unbind:nonDisclosedCardsCountBinding]; } else { … } }
333
SmartBooks
Objective-C und Cocoa – Band 2
Mit diesem neuen Binding können wir jetzt in AppDelegate auch die Zahl der verdeckten Karten binden: for( index = 0; index < playerStacksCount; index++ ) { … // Bindung des Views an den Controller NSString* binding; binding = [NSString stringWithFormat:@"%@%d", playersDisclosedCardsBinding, stackIndex]; [tableBaizeView bind:binding toObject:controller withKeyPath:@"arrangedObjects" options:nil]; // Bindung des Views an den Controller Non-Disclosed-Count binding = [NSString stringWithFormat:@"%@%d", playersNonDisclosedCardsCountBinding, stackIndex]; [tableBaizeView bind:binding toObject:controller withKeyPath:@"filteredOutCount" options:nil]; } … }
Wichtig ist hier, dass wir eben einfach ein weiteres Binding an den Array-Controller einrichten. Es handelt sich also bei den herausgefilterten Karten eigentlich nur um diesselbe Information, die der Array-Controller schon hatte, nur von diesem anders aufbereitet.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 64« von der Webseite herunterladen.
Berechnete Eigenschaften Ein weiteres Standardproblem liegt darin, dass man Eigenschaften hat, die sich aus anderen Eigenschaften ableiten. Denken Sie an die Anzahl der zugedeckten Karten. Diese hängt nämlich von auf den Stapeln verfügbaren Karten ab und dann noch
334
Kapitel 3
View-Controller-Bindung
einmal von der Eigenschaft disclosed der einzelnen Karte. Es ist aber in keinerlei Hinsicht eine neue Information. Für die Spielerkarten hatten wir das Problem gerade mittels einer Subklasse von NSArrayController gelöst. Für das Depot stellt sich das Problem etwas anders dar: Es soll nur zugedeckte Karten zeigen, jedoch nur jede zehnte, wie wir das auch mit Data-Sourcen implementiert hatten. Man könnte das leicht mit den bereits gelernten Mitteln bewerkstelligen: Zum einen nimmt man einen Array-Controller wie gerade, der die zugedeckten Zahlen zählen kann, und einen Value-Transformer, der das Ergebnis durch 10 teilt. Aber wir wollen eine weitere Möglichkeit demonstrieren.
TIPP Viele Wege führen nach Rom: Sie werden zahlreiche Tutorials, Dokumentationen, Bücher finden, die eine solche Beispielapplikation umsetzen, wie wir das hier tun. Nur werden dort Model und View abgestimmt sein. Sie erhalten dann einfache Ergebnisse, was ja kein Wunder ist, wenn alles zusammenpasst. Wir haben hier ja mit voller Absicht ein paar Hürden eingebaut. Dies entspricht auch der Wirklichkeit, weil man eben nicht immer Klassen in den verschiedenen Schichten hat, die aufeinander optimiert sind. Ein »Oh, das funktioniert ja alles so schnell und einfach«-Effekt nach Durcharbeitung eines solchen Textes wird dann schnell zum »Nichts funktioniert mehr« und handgeschriebenen Data-Sourcen. Deshalb machen wir es uns komplizierter. Und mit der Steigerung der Komplexittät steigt natürlich auch die Anzahl der Lösungsmöglichkeiten. Welche Sie später im Projekt wählen, hängt ganz einfach davon ab, was man bei Ihnen am leichtesten implementieren kann. Es gibt bei berechneten (oder abhängigen) Eigenschaften zwei Probleme: Zum einen muss das Model konsistent sein. Dies bedeutet, dass sich die berechnete Eigenschaft ändern muss, wenn sich die Ausgangseigenschaften ändern, von denen sie abhängt. Das ist ein Problem, welches schon innerhalb einer Schicht auftritt. Bei uns wird es etwa pro Stapel eine Eigenschaft nonDisclosedCardsCount geben, die sich konsistent zu den Karten auf dem Stapel verhalten muss. Offenkundig betrifft das nur die Modellklassen CardStack und Card. Das zweite Problem findet sich darin, dass die Änderung der berechneten Eigenschaft mitgeteilt werden muss, damit Bindings funktionieren. Wird an eine berechnete Eigenschaft gebunden, so werden die Bindings einschlagen, wenn eben diese berechnete Eigenschaft mittels eines Setters gesetzt wird, nicht aber, wenn eine Ausgangseigenschaft gesetzt wird, aus der sich die berechnete Eigenschaft ermittelt. Es 335
SmartBooks
Objective-C und Cocoa – Band 2
wird also keine Observierungsnachricht für die Eigenschaft nonDisclosedCards Count des Stapels ausgelöst, wenn die Eigentschaft disclosed einer Karte verändert wird. Bindings hängen immer an einer Eigenschaft einer Instanz. CardStack
cards countOfNonDisclosedCards
Card suit value disclosed
Die Anzahl der zudeckten Karten hängt von den Karten und deren Zustand ab.
Prinzipiell kann man zwei Lösungsansätze für dieses Problem durchführen.
•
In allen Settern derjenigen Eigenschaften, die potentiell die abhängige Eigenschaft ändern, wird eine Methode aufgerufen, die die abhängige Eigenschaft aktualisert und dann setzt. Dies löst gleich beide Probleme, da wir jetzt einen stinknormalen Setter haben, den man observieren kann.
•
Wir speichern die berechnete Eigenschaft gar nicht ab, sondern bieten nur einen Getter. Dieser berechnet bei jedem Aufruf den aktuellenWert. Dies löst das Problem der möglichen Inkonsistenz, da die Berechnung ja immer aktuell ist. Allerdings wird jetzt für die berechnete Eigenschaft kein Setter benutzt, so dass niemand eine Änderung mitbekommt.
Wir werden die verschiedenen Varianten probieren. Als Erstes richten Sie bitte bei der Karte eine Eigeschaft ein, die den Stapel, auf dem die Karte liegt, in Bezug nimmt: @class CardStack; @interface Card : NSObject { CardStack* stack; … } @property( assign ) CardStack* stack;
Das assign erklärt sich daraus, dass wir einen Rückverweis haben, der kein retain auslösen darf (Band I, S. 249 ff.) Die Accessoren synthetisieren wir zunächst: @implementation Card @synthesize stack; @synthesize value, suit, disclosed;
336
Kapitel 3
View-Controller-Bindung
Wir müssen jetzt bei jeder Änderung der Karten im Stapel entsprechend den Rückverweis setzen. Da schreiben wir uns eigene Accessoren für cards anstelle der bisher synthetisierten: #import "Card.h" @implementation CardStack - (NSArray*)cards { return cards; } - (void)setCards:(NSArray*)value { if( value != cards ) { NSEnumerator* cardsEnum; // alle alten Karte aus dem Stack entfernen cardsEnum = [self.cards objectEnumerator]; for( Card* card in cardsEnum ) { card.stack = nil; } // Neue Karten setzen cardsEnum = [value objectEnumerator]; for( Card* card in cardsEnum ) { card.stack = self; } [cards release]; cards = [value copy]; } }
Wir ändern drei Actionmethoden, damit die etwas mit dem Depotstapel herumspielen: - (IBAction)addCard:(id)sender { CardStack* stack; stack = [self.stacks objectAtIndex:depotStackIndex]; Card* card = [[[Card alloc] init] autorelease]; NSMutableArray* cards = [stack mutableArrayValueForKey:@"cards"]; card.value = [cards count] + 1; [cards insertObject:card atIndex:0]; }
337
SmartBooks
Objective-C und Cocoa – Band 2
… - (IBAction)setStack:(id)sender { CardStack* stack; stack = [self.stacks objectAtIndex:depotStackIndex]; NSMutableArray* newCards = [NSMutableArray array]; NSUInteger value; Card* card; for( value = 1; value < 7; value++ ) { card = [[[Card alloc] init] autorelease]; card.value = value; [newCards addObject:card]; } stack.cards = newCards; } - (IBAction)discloseCards:(id)sender { CardStack* stack; stack = [self.stacks objectAtIndex:depotStackIndex]; … }
Die Methoden verrichten jetzt ihre Arbeit einfach auf dem Depot. Um die geöffneten Karten nicht zu sehen, binden wir jetzt nur zu Testzwecken den Depotstapel über unseren neuen Controller. Wir benötigen das eigentlich nicht, da der Depotstapel ja nie eine aufgedeckte Karte zeigen wird. Aber so können wir die Richtigkeit unseres Codes besser erkennen. Außerdem binden wir jetzt die Eigenschaft der verdeckten Karten eben nicht an diesen Controller, sondern an die entsprechende Eigenschaft des Stapels. Dass es über den Controller geht, wissen wir ja schon von den Spielerstapeln. Zunächst der Export des Bindings aus TableBaizeView: extern NSString* const depotDisclosedCardsBinding; extern NSString* const depotNonDisclosedCardsCountBinding;
mit der dazugehörigen Implementierung: NSString* const depotDisclosedCardsBinding = @"depotDisclosedCardsBinding";
338
Kapitel 3
View-Controller-Bindung
NSString* const depotNonDisclosedCardsCountBinding = @"depotNonDisclosedCardsCountBinding"; - (void)
bind:(NSString*)binding toObject:(id)observed withKeyPath:(NSString*)keyPath options:(NSDictionary*)options
{ CardStackView* stackView; if( binding == targetDisclosedCardsBinding ) { … } else if( binding == depotDisclosedCardsBinding ) { … } else if( binding == depotNonDisclosedCardsCountBinding ) { stackView = [self viewWithStackIndex:depotStackIndex]; [stackView bind:nonDisclosedCardsCountBinding toObject:observed withKeyPath:keyPath options:options]; } else if( [binding hasPrefix:playersDisclosedCardsBinding] ){ … } else if( [binding hasPrefix:playersNonDisclosedCardsCountBinding] ){ … } else { … } } - (void)unbind:(NSString*)binding { CardStackView* stackView; if( binding == targetDisclosedCardsBinding ) { … } else if( binding == depotDisclosedCardsBinding ) { … } else if( binding == depotNonDisclosedCardsCountBinding ) { stackView = [self viewWithStackIndex:depotStackIndex]; [stackView unbind:nonDisclosedCardsCountBinding]; } else if( [binding hasPrefix:playersDisclosedCardsBinding] ){
339
SmartBooks
Objective-C und Cocoa – Band 2
… } else if( [binding hasPrefix:playersNonDisclosedCardsCountBinding] ){ … } else { … } }
Im AppDelegate binden wir jetzt entsprechend den Depotstapel: - (void)awakeFromNib { … // Depotstack binden // Controller NSMutableArray* controllers = [NSMutableArray array]; NSPredicate* filter; filter = [NSPredicate predicateWithFormat:@"disclosed==YES"]; NSArrayController* controller; controller =[[[DisclosedFilterArrayController alloc] init] autorelease]; [controller setAutomaticallyRearrangesObjects:YES]; [controllers addObject:controller]; [controller setFilterPredicate:filter]; // Controller an Stack binden CardStack* depotStack = [self.stacks objectAtIndex:depotStackIndex]; [controller bind:NSContentArrayBinding toObject:depotStack withKeyPath:@"cards" options:nil]; // Bindung des Depotviews an den Controller Karten [tableBaizeView bind:depotDisclosedCardsBinding toObject:controller withKeyPath:@"arrangedObjects" options:nil];
340
Kapitel 3
View-Controller-Bindung
// ... und Anzahl [tableBaizeView bind:depotNonDisclosedCardsCountBinding toObject:depotStack withKeyPath:@"countOfNonDisclosedCards" options:nil]; NSUInteger index; // Bitte zwei Zeilen loeschen for( index = 0; index < playerStacksCount; index++ ) { … } … }
Soweit zu den Vorarbeiten. Überlegen wir uns jetzt einmal, unter welchen Umständen sich die Anzahl der verdeckten Karten auf einem Stapel verändern kann:
•
Eine Karte auf dem Stapel ändert ihren Zustand disclosed, wird also aufgedeckt oder wieder zugedeckt.
•
Eine Karte wird dem Stapel hinzugefügt, von ihm entfernt oder gegen eine andere Karte ausgestauscht.
•
Der gesamte Stapel, also die Eigenschaft cards von CardStack, wird neu gesetzt.
Lösung 1: Neu berechnen und setzen Die erste Lösung besteht darin, dass wir dem Stack eine Eigenschaft countOfNonDisclosedCards geben und bei jeder möglichen Änderung dieser Eigenschaft diese aktualisieren. Fügen wir zunächst die Eigenschaft in CardStack.h ein: @interface CardStack : NSObject { NSArray* cards; NSUInteger countOfNonDisclosedCards; } @property( copy ) NSArray* cards; @property NSUInteger countOfNonDisclosedCards; @end
deren Accessoren wir einfach synthetisieren: @implementation CardStack @synthesize countOfNonDisclosedCards;
341
SmartBooks
Objective-C und Cocoa – Band 2
In Card.m bauen wir dann einen entsprechende Setter, die die Änderung vornehmen: #import "CardStack.h" @implementation Card @synthesize value, suit; // disclosed und stack entfernen - (CardStack*)stack { return stack; } - (void)setStack:(CardStack*)newStack { // nichts aendert sich if( newStack == stack ) { return; } // Ist die Karte zugedeckt if( self.disclosed == NO ) { // Falls Sie sich in einem Stack befindet, muss dessen // Zaehler herabgesetzt werden. if( stack ) { self.stack.countOfNonDisclosedCards--; } // Wird sie einem neuen Stack hinzugefuegt, so muss der // Zaehler erhoeht werden if( newStack ) { newStack.countOfNonDisclosedCards++; } } stack = newStack; // Assign-Setter! } - (BOOL)disclosed { return disclosed; } - (void)setDisclosed:(BOOL)newDisclosed { // Befindet sich die Karte in einem Stack if( self.stack ) { // Wird die Karte aufgedeckt if( (disclosed == NO) && (newDisclosed == YES) ) { self.stack.countOfNonDisclosedCards--;
342
Kapitel 3
View-Controller-Bindung
// zugedeckt? } else if( (disclosed == YES) && (newDisclosed == NO) ) { self.stack.countOfNonDisclosedCards++; } } disclosed = newDisclosed; }
Hier spielt also wirklich die Musik: Bei einer Änderung der disclosed-Eigenschaft einer Karte ändert der Setter automatisch die abhängige Eigenschaft des Stapels. Wenn Sie die Applikation starten, sollten Sie mit den oben erwähnten Actionmethoden den Depotstapel verändern können.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 65« von der Webseite herunterladen. Lösung 2: Mitteilen und neu berechnen Eine andere Lösung ist typisch für Key-Value-Observing. Der Grundgedanke liegt darin, dass Eigenschaften nur dann ausgerechnet sein müssen, wenn sie abgefragt werden. Daher kann ich sie in einem Getter berechnen. Ich benötige dann keine Instanzvariable mehr und auch keinen Setter. Allerdings müssen wir die Observierungsnachricht selbst auslösen, da wir eben keinen Setter mehr benutzen. setCards::
CardStack cards countOfNonDisclosedCards
Card suit value disclosed
setDisclosed:
countOfNonDisclosedCards-Change KVO-Engine observers
Es wird keine Änderung mehr durchgeführt, sondern dieselbe nur mitgeteilt.
Das bauen wir zunächst in CardStack ein. Zunächst entfernen wir die Instanzvariable und setzen die Eigenschaft auf nur lesen:
343
SmartBooks
Objective-C und Cocoa – Band 2
@interface CardStack : NSObject { NSArray* cards; } @property( copy ) NSArray* cards; @property( readonly ) NSUInteger countOfNonDisclosedCards; @end
In CardStack.m implementieren wir das entsprechend den Anforderungen unter Aufgabe des @synthesize: @implementation CardStack - (NSUInteger)countOfNonDisclosedCards { NSUInteger index = 0; for( index = 0; index < [self.cards count]; index++ ) { if( [[self.cards objectAtIndex:index] disclosed] == YES ) { break; } } return index; }
In Card.m muss entsprechend die Änderung der Eigenschaft herausgenommen werden. Sie lässt sich ja nicht mehr schreiben. Wir können daher die Accessoren für stack: synthetisieren lassen. Außerdem reduzieren wir den Setter für disclosed: @synthesize value, suit; @synthesize stack; - (BOOL)disclosed { return disclosed; } - (void)setDisclosed:(BOOL)newDisclosed { disclosed = newDisclosed; }
Die Implementierung kennen Sie der Sache nach ja schon. Wir können jedoch nicht den Setter für disclosed synthetisieren, da wir gleich noch etwas in ihm erledigen müssen. Damit wäre eigentlich alles getan, um aus dem Code heraus die Eigenschaft abfragen zu können. Aber das Problem liegt darin, dass niemand von einer Änderung 344
Kapitel 3
View-Controller-Bindung
der Eigenschaft erfährt, wenn der Stapel geändert wird. Hier muss manuell Key-Val ue-Observing ausgelöst werden. Sie haben eine Möglichkeit, dies zu tun, bereits im ersten Band in Bezug auf Managed-Objects von Core Data kennengelernt. Hier ist ja das automatische Key-Value-Observing ausgeschaltet (Band I, S. 627). Allerdings liegt hier der Hund anders begraben: Zwar haben wir automatisches Key-Value-Observing, jedoch schaut sich dies ja die Setter einer Eigenschaft an. Es wird also immer nur eine Eigenschaft beobachtet, deren Setter benutzt wurde. Wir haben jedoch gar keinen Setter mehr für unsere Eigenschaft. Wenn Sie also den Depotstapel über die Actionmethoden ändern, dann werden Sie auf dem Bildschirm nichts vorfinden, weil sich die gebundene Eigenschaft des CardStackView nicht synchronisiert. Es gibt zwei Lösungen aus dem Dilemma, die sich ergänzen. Die erste Methode erlaubt es, bestimmte Eigenschaften als abhängig zu bestimmen (»halbautomatisches Key-Value-Observing«). Das Key-Value-Observerving löst dann automatisch eine Observierungsnachricht aus, wenn sich die Originaleigenschaft ändert. Hierzu dienen die Methoden +keyPathsForValuesAffectingValueForKey: und +keyPathsForValuesAffectingKey, welche Klassenmethoden sind, da die Abhängigkeit von Eigenschaften ja spezifisch für eine Klasse ist. Die erste Methode sucht einfach nach der zweiten Methode für den Parameter key. Demnach reicht es aus, diese zweite Methode in CardStack.m zu implementieren: + (NSSet*)keyPathsForValuesAffectingCountOfNonDisclosedCards { return [NSSet setWithObject:@"cards"]; } @end
Wie Sie an der Implementierung leicht erkennen können, muss ein Set zurückgeliefert werden, welches sämtliche Schlüsselpfade derjenigen Eigenschaften enthält, die die abhängige Eigenschaft verändern. In unserem Falle werden also alle Änderungen der Ausgangseigenschaft cards zum Anlass genommen, die Obervierungsmethode für die abhängige Eigenschaft countOfNonDisclosedCards auszulösen. Damit funktionieren das Einfügen von Karten und das Setzen. Allerdings ist die Actionmethode discloseCards: immer noch fehlerhaft: Die aufgedecke Karte wird als zusätzliche angezeigt, da sie nunmehr zwar durch den Array-Controller läuft. Die Wert von countOfNonDisclosedCards wird aber nicht vermindert. Wer auf den Gedanken kommt, einfach den Schlüsselpfad cards.disclosed hinzuzufügen, wird leider enttäuscht. Zwar ist es grundsätzlich möglich, Schlüsselpfade der Originaleigenschaften zu übergeben. Aber diese dürfen nicht durch eine To-manyRelationship laufen.
345
SmartBooks
Objective-C und Cocoa – Band 2
Deshalb verwenden wir hier den zweiten Weg und lösen hier die Observierung selbst aus (manuelles Key-Value-Observing). Es sei hier angemerkt, dass man generell diese Methoden verwenden kann, um Aktualisierungen zu erreichen. Das ist aber untunlich. Die Observierungsmethoden sollten wirklich nur dann ausgelöst werden, wenn inhaltlich eine Änderung vorliegt. Dies ist für das Aufdecken der Karte recht simpel in Card.m zu implementieren. Wir müssen allerdings die Struktur ändern, um die obige Bedingung möglichst einzuhalten: - (void)setDisclosed:(BOOL)newDisclosed { // Befindet sich die Klasse in einem Stack und haben wir // inhaltlich eine Aenderung BOOL change; change = (newDisclosed != disclosed) && (self.stack != nil); NSString* key = @"countOfNonDisclosedCards"; // Aenderung ankuendigen if( change ) { [self.stack willChangeValueForKey:key]; } // Aenderung durchführen disclosed = newDisclosed; // Aenderung abschliessen if( change ) { [self.stack didChangeValueForKey:key]; } }
Dieser Code gibt auch gut die Struktur des manuellen Key-Value-Codings wieder: Änderung ankündigen, Änderung durchführen, Änderung abschließen. Wirklich Änderung durchführen? Ja, denn die Änderung der Eigenschaft disclosed einer Karte ist eben gleichzeitig die Änderung der abhängigen Eigenschaft countOfNonDisclosedCards des Stapels. Jetzt klappt es auch mit auch mit dem Aufdecken. Dennoch haben wir noch ein Problem, dessen Lösung nicht offensichtlich ist: Die Eigenschaft stack einer Karte verändert ja auch die Anzahl der Eigenschaft countOfNonDisclosedCards, nämlich dann, wenn die Karte zugedeckt war. Dieser Setter ist jedoch nur zur Konsistenzgewährleistung da. Er darf also eigentlich nur von setCards: des Stapels aufgerufen werden. Wird er unmittelbar ausgeführt, so bekommen wir ohnehin ein Konsistenzproblem. 346
Kapitel 3
View-Controller-Bindung
GRUNDLAGEN Wie kann man eigentlich einigermaßen sicher eine Methode verstecken? Eine Möglichkeit besteht darin, eine private Kategorie in einer eigenen Datei zu bilden, die man nicht dokumentiert und nur selbst importiert. Allerdings sollte unser Depotstapel nur ein Zehntel der Karten anzeigen. Jetzt wird wieder jede dargestellt. Natürlich kann man sich hier wieder einen Value-Transformer bauen, der einen Integer durch 10 teilt. Das kennen Sie ja schon. Wir gehen zum Abschluss dieses Themas einen anderen Weg und bauen einfach eine Subklasse von CardStack, die wir »DepotStack« nennen. Legen Sie bitte diese neue Klasse an und wählen Sie als Vorlage Objective-C class | NSObject. In dieser legen wir uns einfach eine neue Eigenschaft an: #import "CardStack.h" @interface DepotStack : CardStack { } @property( readonly ) NSUInteger count10OfNonDisclosedCards; @end
In der Implementierung wird es naturgemäß leicht: @implementation DepotStack - (NSUInteger)count10OfNonDisclosedCards { return self.countOfNonDisclosedCards / 10; } #pragma mark Class Methods + (NSSet*)keyPathsForValuesAffectingCount10OfNonDisclosedCards { return [NSSet setWithObject:@"countOfNonDisclosedCards"]; }
Beachten Sie bitte, dass unsere Karte ja nur eine Änderung der Basiseigenschaft countOfNonDisclosedCards triggert. Diese neue Eigenschaft der Subklasse würde also nicht als geändert markiert. Das ist jedoch kein Problem, da wir auch in der Subklasse abhängige Eigenschaften definieren können, sogar dann, wenn die Originaleigenschaft aus der Basisklasse stammt. Ebenfalls ist es, wie ersichtlich, möglich, eine abhängige Eigenschaft von einer abhängigen Eigenschaft zu bestimmen.
347
SmartBooks
Objective-C und Cocoa – Band 2
(Im Wesentlichen erzeugt dies eine Observierung, die die abhängige Observierungsmethode auslöst.) Im Application-Delegate müssen wir dann entsprechend eine Instanz der Subklasse erzeugen: #import "CardStack.h" #import "DepotStack.h" … - (void)stacksWithDealtCards:(NSMutableArray*)heap { … for( stackIndex = 0; stackIndex < 12; stackIndex++ ) { CardStack* stack; if( stackIndex == depotStackIndex ) { stack = [[[DepotStack alloc] init] autorelease]; } else { stack = [[[CardStack alloc] init] autorelease]; } [newStacks addObject:stack]; } … } - (void)awakeFromNib { … [tableBaizeView bind:depotNonDisclosedCardsCountBinding toObject:stack withKeyPath:@"count10OfNonDisclosedCards" options:nil]; … }
Jetzt sollte der Stapel auch wieder reduziert angezeigt werden.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 66« von der Webseite herunterladen.
348
Kapitel 3
View-Controller-Bindung
Delegating HILFE Wir haben wieder ein Projekt »Card Game 67« auf die Webseite gestellt, welches um die Actions abgespeckt ist. Als Letztes muss unsere Applikation die Spielregeln lernen. Wie bereits eingangs des Kapitels dargestellt, wäre es freilich eine Möglichkeit, Subklassen unserer Views zu erstellen, die das entsprechende Wissen modellieren. Und wie bereits gesagt, liegt der Nachteil darin, dass in einer Subklasse alles, was wir so schön in der Kapsel verborgen haben, sichtbar wird (White-Boxing). Basisklasse ivars … -mouseDown: -disclose: …
Zugriff Subklasse -mouseDown: -disclose …
Bei einer Subklasse erhält der anpassende Code Zugriff auf die Instanzvariablen.
GRUNDLAGEN Wie Sie in der Referenz sehen werden, kann der Zugriff auf die Instanzvariablen in Subklassen eingeschränkt werden. Dies ändert aber nichts an dem grundlegenden Problem, dass sich die Basisklasse öffnet und sichtbar wird. Delegating beschränkt die Anpassung ja auf einen ganz bestimmten Methodensatz. Die Alternative ist eben Delegating. Und bereits im ersten Band wurde ausgeführt, dass es in aller Regel der bessere Weg ist, Delegatemethoden zur verwenden, wenn ein Verhalten einer Klasse angepasst werden soll. Voraussetzung ist es freilich, dass die entsprechende anzupassende Klasse Delegating unterstützt. Denn die Verhaltensänderung kann nur über das entsprechende API des Delegierers erfolgen. Und genau das ist jetzt unsere Aufgabe: Eine entsprechende API zu definieren.
349
SmartBooks
Objective-C und Cocoa – Band 2
Delegierer ivars … -mouseDown: -disclose …
Delegate -shouldDo… -didDo… …
Bei Delegating erhält das Delegate nur ganz bestimmte Nachrichten.
GRUNDLAGEN Wie Sie den Abbildungen entnehmen können, existiert ein zweiter Unterschied: Ableitung erfolgt in Bezug auf Klassen, Delegation in Bezug auf Instanzen. Es ist ein Leichtes, ein Delegate auszutauschen. Haben wir Subklassen verwendet, so müssen die Objekte erneut erzeugt werden. Dies bereitet aber Probleme, wenn nicht bekannt ist, wer alles das Ursprungsobjekt referenziert.
Definition der API Strukturelle Überlegungen Bei der Definition der Delegate-API kann man die Sache von zwei Seiten angehen: Eine Möglichkeit besteht darin, bereits Low-Level-Funktionen dem Delegate zugänglich zu machen. In unserem Falle hieß dies etwa, jeden Mausklick an das Delegate zur Verarbeitung weiterzuleiten. Hiermit würden wir jedoch wieder viel Information preisgeben müssen, damit das Delegate seine Arbeit machen kann, allerdings die falsche, weil etwa die Mausposition einen internen Bezug im View hat. Etwa müsste das Delegate ermitteln können, welche Karte sich unter der Maus befand. Das macht die API nicht nur fetter, sondern verringert den Vorteil der Kapselung. In der Regel ist es deutlich besser, das Delegate nur funktional vorgekaut zu benachrichtigen. Eine entsprechende Delegatemethode würde etwa für »Auf Karte geklickt« oder besser »Karte aufdecken«, nicht für das Ereignis »Mausklick« geschickt. Hier kann ich ein sauberes, funktionales Interface programmieren und gebe dem Delegate kontrolliert Informationen heraus. Namensgebung Auch wenn nicht zwingend vorgeschrieben, so hat sich doch ein System für die Benennung und Signatur von Delegatemethoden herauskristallisiert: Der Metho350
Kapitel 3
View-Controller-Bindung
denname beginnt mit der Klassenbezeichnung der sendenden Instanz, die auch zugleich den ersten Parameter bildet. Hiernach folgt zumeist ein Hilfsverb (should, will, did), welches die Art der Delegatemethode bekanntgibt:
•
should-Methoden ermöglichen eine Einflussnahme auf die auszuführende Aktion, indem sie einen Returnwert liefern. Dies kann ein BOOL sein, der Auskunft gibt, ob die Aktion durchgeführt werden soll oder ein Integer, der gleich einen Wert liefert. Denken Sie hier etwa an Selektierungen.
•
will-Methoden werden ebenfalls vor der Aktion ausgeführt, ermöglichen jedoch keine Änderung der Aktion mehr. Vielmehr kann hier (lediglich) eigenes Verhalten hinzugefügt werden.
•
did-Methoden haben eine ähnliche Funktion wie will-Methoden. Allerdings wird die entsprechende Nachricht erst nach der Durchführung der Operation gesendet.
•
Daneben kann es weitere Methoden geben, die insbesondere Aufforderungen an das Delegate enthalten, etwas zu tun. Hier überlässt der Delegierer seine Arbeit dem Delegierten. Bei uns ist das etwa die Aufforderung, die gezogenen Karten ins Pasteboard zu schreiben.
Es ist keinesfalls so, dass sämtliche Methoden angeboten werden müssen. So würde bei einem Drag etwa zwischen der will-Nachricht und der did-Nachricht lediglich das rein graphische (und virtuelle) Entfernen der Karten vom View nebst Markierung zum Neuzeichnen liegen, also ein graphischer Effekt. Mit anderen Worten: Was genau angeboten wird, ist Tatfrage und lässt sich nicht allgemein sagen. Sie sollten aber die typischen Aufgaben Ihres Delegates im Blick haben, um so wenigstens einen Mindestsatz an Delegatemethoden anzubieten. Definition Hiervon ausgehend lassen sich die Methoden bestimmen. Wie bei der Data-Source verwenden wir ein Protokoll zur Publikation der Methoden. Dies ist seit Leopard (OS X 10.5) problemlos möglich, da in einem Protokoll auch optionale Methoden stehen können. Bedenken Sie, dass anders als bei der Data-Source die Delegatemethoden implementiert werden können, jedoch nicht müssen. Wie das DataSource-Protokoll binden wir das einfach in den Header TableBaizeView.m ein: extern NSString* const playersNonDisclosedCardsCountBinding; @class TableBaizeView; @protocol TableBaizeViewDelegate @optional
351
SmartBooks
Objective-C und Cocoa – Band 2
// Aufdecken - (BOOL) tableBaizeView:(TableBaizeView*)view discloseCardOnStackWithIndex:(NSUInteger)index; …
Hier sehen Sie ein Beispiel für eine Methode aus der letzten Gruppe. Das Öffnen der Karte geschieht ja im Model, die Views müssen das lediglich aktualisieren. Anders ist das etwa, wenn ein Disclosure in einem Outlineview geöffnet wird. Hier liegt keine Änderung am Model vor, sondern der Darstellung auf dem Schirm, weshalb die entsprechenden Methoden should im Namen tragen. … // Geben - (void)tableBaizeViewDealCards:(TableBaizeView*)view; …
Genau so verhält es sich auch hier. Zusätzlich tritt hier ein Problem auf, welches mit der Syntax von Objective-C zusammenhängt: Erhält eine Delegatemethode neben dem Absender (Delegierer) keinen weiteren Parameter, so lässt sich der Parameter nur an das Ende des Methodennamens platzieren. Die Syntax von Objective-C lässt einen Methodennamen wie tableBaizeView:dealCards (kein Doppelpunkt am Ende!) mit dem Parameter in der Mitte nicht zu. Das ist sprachlich etwas unschön. Wenn Sie allerdings ein paar Mal Delegating benutzt haben, dann gewöhnt sich Ihr Rückenmark daran, dass der erste Parameter stets den Absender bezeichnet, ohne dass Sie noch auf den Methodennamen blicken. Auf einen anderen Gedanken möchten wir ebenfalls eingehen: Für einen Klick auf das Depot haben wir ersichtlich eine eigene Methode vorgesehen. Natürlich könnte man hier auch daran denken, tableBaizeView:shouldDiscloseCardOnStackWith Index: zu verwenden, und einen entsprechenden Index für den Depotstapel übergeben. Es soll bloß gezeigt werden, dass man eben auf dasselbe Ereignis (Mausklick) unterschiedliche Nachrichten schicken kann, halt funktional strukturiert, nicht im Hinblick auf die Ereignisart. … // Ziehen - (BOOL) tableBaizeView:(TableBaizeView*)view shouldDragCardsAtIndex:(NSUInteger)cardIndex fromStackWithIndex:(NSUInteger)stackIndex; - (BOOL)
tableBaizeView:(TableBaizeView*)view writeCardsAtIndex:(NSUInteger)cardIndex
352
Kapitel 3
View-Controller-Bindung
fromStackWithIndex:(NSUInteger)stackIndex toPasteboard:(NSPasteboard*)pasteboard; - (BOOL)
tableBaizeView:(TableBaizeView*)view shouldValidateDrop:(id)dragInfo onStackWithIndex:(NSUInteger)stackIndex;
- (void)
tableBaizeView:(TableBaizeView*)view performDrop:(id)dragInfo onStackWithIndex:(NSUInteger)stackIndex;
- (void)
tableBaizeView:(TableBaizeView*)view removeCardsAtIndex:(NSUInteger)index onStackWithIndex:(NSUInteger)stackIndex;
@end @interface TableBaizeView : NSView {
Eigentlich sollten die Methodenbezeichnungen selbstsprechend sein. Womit wir gleich beim nächsten Punkt wären: Wir adressieren hier die einzelnen Stapel und deren Karten mittels eines Indexes. Man könnte sich freilich auch die entsprechenden Stapel und Karten abholen, entweder über die Data-Source oder über die Bindings. Da jedoch der View die Karten nicht umsortiert, sondern die Reihenfolge vom Model vorgegeben wird, besteht dafür keine Veranlassung – anders etwa als beim Tableview, das eine eigene Sortierung enthalten kann, die unabhängig vom Model ist. Zuletzt geben wir dem View noch eine Eigenschaft für das Delegate. Zu beachten ist hierbei, dass ein Delegate kein retain erhält (Gefahr der Retainzyklen). Da wir in Objective-C 2.0 @optional haben, können wir auch hier das Delegate mit dem Protokoll typisieren: @interface TableBaizeView : NSView { … id delegate; } @property( assign ) id delegate;
Und die entsprechende Synthetisierung in der Implementierung: @implementation TableBaizeView @synthesize delegate;
353
SmartBooks
Objective-C und Cocoa – Band 2
Das Delegate melden wir dann im Applikationsdelegate an. Dazu wollen wir es zunächst entsprechend typisieren: @interface AppDelegate : NSObject { … }
und dann entsprechend in -awakeFromNib (AppDelegate) übergeben: - (void)awakeFromNib { [tableBaizeView setDelegate:self]; … }
Damit sind die Vorarbeiten erledigt.
Delegating implementieren Als nächsten Schritt müssen wir die Unterstützung des Delegates anbieten, also in bestimmten Situationen versuchen, Nachrichten an das Delegate zuzustellen. Hier haben wir die Problematik, dass wir den Empfänger der Events (CardStackView) verstecken und wir über das Front-End TableBaizeView die Nachrichten verschicken wollen. Also müssen die Subviews das TableBaizeView informieren.
AUFGEPASST Sehen Sie bitte das System: Der Kartenstapel nimmt nur die Events an, der Tablebaizeview kann aufgrund seiner strukturellen Information daraus etwas Strukturelles machen und das Delegate bestimmt schließlich die konkrete Ausführung.
Klick auf Depot: neue Karten Kümmern wir uns zunächst um das Austeilen neuer Karten. Dies ist recht einfach zu implementieren. Wir können zwei Ausgangspunkte überlegen. Zum einen wäre es möglich, dass der Card-Stack-View -mouseDown: implementiert, also das Event bekommt. Es müsste dann bei sich die entsprechende Karte heraussuchen und an den TableBaizeView eine entsprechende Nachricht senden. Das TableBaizeView würde dann nachschauen, von welchem Stapel die Nachricht kommt, und entsprechend entweder veranlassen, dass eine Karte aufgedeckt wird (Klick auf Spielerstapel) oder neue Karten ausgeteilt werden (Klick auf Depotstapel). 354
Kapitel 3
View-Controller-Bindung
Aber es geht auch anders, da wir ja eine Responder-Chain haben: Wir lassen einfach in den Stapeln -mouseDown: unimplementiert, so dass es zum Table-BaizeView weitergereicht wird. Dort fangen wir das Event ab und ermitteln den Stapel. Hier muss lediglich festgestellt werden, auf welche Karte des Stapels geklickt wurde, da nur die oberste »aktiv« sein soll. Ist dies der Fall, wird versucht, eine entsprechende Nachricht an das Delegate zuzustellen. mouseDown:
TableBaizeView delegate allowDragging… … tableBaizeViewDealCards: Delegate …
Im einfachsten Falle spricht der TableBaizeView unmittelbar mit dem Delegate.
Zunächst muss also die Methode -indexOfCardAtLocation: (CardStackView) publik gemacht werden, damit sie in TableBaizeView benutzt werden kann: @interface CardStackView : NSView { … - (NSInteger)indexOfCardAtLocation:(NSPoint)location; @end
In TableBaizeView.m implemenetieren wir die Eventmethode nach der Methode -unbind und spulen das oben skizzierte Programm ab: - (void)unbind:(NSString*)binding { … } - (void)mouseDown:(NSEvent*)event { // Auf welchen Stapel wurde geklickt? NSPoint location = [event locationInWindow]; location = [[self superview] convertPoint:location fromView:nil]; NSView* view = [self hitTest:location];
355
SmartBooks
Objective-C und Cocoa – Band 2
if( view == self ) { return; } CardStackView* stackView = (CardStackView*)view; location = [stackView convertPoint:location fromView:[self superview]]; …
Zunächst ermitteln wir also den Stapel, auf den geklickt wurde. Dazu bedienen wir uns der Methode -hitTest:. Diese findet das in der Hierarchie tiefstgelegene View, welches sich unter der Maus befindet. Sämtliche uns interessierende Views sind Subviews des Table-Baize-Views. Daher beginnen wir die Suche dort. Eine Besonderheit liegt allerdings darin, dass die Koordinaten nicht etwa in dem Koordinatensystem des Views angegeben sein müssen, von dem die Suche aus startet. Vielmehr beziehen sie sich auf dessen Superview. Falls ein Subview gefunden wurde, also nicht in den Hintergrund auf den Filz geklickt wurde, so werden die Koordinaten schließlich in dessen System umgerechnet. … // Wurde auf die letzte Karte geklickt? NSUInteger cardsCount = [[stackView cards] count]; cardsCount += [stackView countOfNonDisclosedCards]; if( cardsCount == 0 ) { return; } NSUInteger cardIndex; cardIndex = [stackView indexOfCardAtLocation:location]; if( cardIndex != (cardsCount - 1) ) { return; } …
Mit dieser Koordinatenangabe wird der Index der angeklickten Karte bestimmt. Handelt es sich nicht um die oberste Karte, so wird wiederum die Methode abgebrochen. Zu beachten ist hier wieder, dass der Index sich aus der Anzahl der offenen Karten zuzüglich der geschlossenen Karten errechnet. Wir speichern in dem Kartenarray des Stapels ja lediglich die offenen Karten.
356
Kapitel 3
View-Controller-Bindung
… // Im Falle des Depotstapels, die Delegatemethode ausfuehren if( stackView == depotView ) { id myDelegate = self.delegate; SEL dealSelector = @selector( tableBaizeViewDealCards: ); if( [myDelegate respondsToSelector:dealSelector] ) { [myDelegate tableBaizeViewDealCards:self]; } } }
AUFGEPASST Übrigens ist es notwendig(!), dass die Referenz auf das Delegate zunächst in die lokale Variable myDelegate vom Typ id ohne den Zustaz des Protokolles kopiert wird. Ansonsten akzeptiert der Compiler nämlich nur noch Nachrichten aus dem Protokoll. Damit funktioniert aber -respondsToSelector: nicht mehr. Weitere Informationen hierzu haben wir in die Referenz gepackt. Zum Schluss erfolgt dann die eigentliche Arbeit. Handelt es sich um den Depotstapel, so wird das Delegate befragt, ob es die vorgesehene Methode implementiert, und diese wird ausgeführt, wenn dies der Fall ist. Denken Sie daran, dass, auch wenn wir die Instanzvariable für das Delegate typisiert hatten, das Protokoll ja die Implementierung der Methode nicht zwingend vorschreibt. Die Nachfrage ist also geboten! Sie können daher das Programm jetzt schon starten, ohne dass es abzustürzen droht. Es passiert nur einfach nichts. Im letzten Schritt müssen wir freilich die Delegatemethode im Application-Delegate implementieren: @synthesize filterControllers; - (void)tableBaizeViewDealCards:(TableBaizeView*)view { NSUInteger stackIndex; NSUInteger counter; // Depotkarten holen CardStack* depotStack = [[self stacks] objectAtIndex:depotStackIndex]; NSMutableArray* depotCards; depotCards = [depotStack mutableArrayValueForKey:@"cards"];
357
SmartBooks
Objective-C und Cocoa – Band 2
for( counter = 0; counter < playerStacksCount; counter++ ) { stackIndex = counter + playerStacksMinIndex; // Spielerkarten holen CardStack* playerStack; playerStack = [[self stacks] objectAtIndex:stackIndex]; NSMutableArray* playerCards; playerCards = [playerStack mutableArrayValueForKey:@"cards"]; // Aufdecken und Verschieben Card* card = [depotCards lastObject]; card.disclosed = YES; [depotCards removeLastObject]; [playerCards addObject:card]; } }
Bereits die eingestreuten Kommentare dürften als Erläuterung ausreichen, zumal Sie die Implementierung von Delegatemethoden bereits aus dem ersten Band kennen. Übersetzen und starten Sie das Programm. Bei einem Klick auf die letzte Karte des Depots sollte jeweils eine neue Karten auf den Spielerstapeln erscheinen.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 68« von der Webseite herunterladen.
Dragging starten Schwieriger gestaltet sich schon die Implementierung von Drag & Drop. Zum einen ist diese Aktion ja in mehrere Nachrichten zerstückelt. Aber das werden wir ebenso aufteilen. Zum anderen müssen hier die Views deutlich enger zusammenarbeiten. Denn nur die Card-Stack-Views können das Drag-Image herstellen, während der Table-Baize-View den Überblick hat, welcher Stapel wie aktiv werden soll. Schließlich kennt das Delegate die Spielregeln und gibt seinen abschließenden Senf dazu.
358
Kapitel 3
View-Controller-Bindung
mouseDragged
CardStackView superview -mouseDragged: … allowDraggingFromCardStackView: atIndex: TableBaizeView delegate allowDragging… … tableBaizeView: shouldDragCardsAtIndex: fromStackWithIndex: Delegate …
Das Event wird durch die Verantwortungsebenen gereicht.
Die Entscheidungsebenen beim Delegating Sozusagen als Gegenbeispiel programmieren wir diesmal die Angelegenheit vom Stapel aus, wobei inhaltlich die Hauptverantwortung bei unserer Klasse TableBaizeView als Front-End bleibt. Der Card-Stack-View sorgt lediglich für das Image und den Start des Zyklusses. Dabei werden Drag-Events auf mehreren Ebenen gefiltert. CardStackView Akzeptiert nur Drags, die auf einer offenen Karte erfolgen.
TableBaizeView Akzeptiert nur Drags, die auf einem Spielerstapel erfolgen.
Delegate Akzeptiert nur Drags, die den Regeln entsprechen.
Operation
Aus einem Eventstrom wird ein Rinnsal.
•
Die Klasse CardStackView kann nur mit Drags umgehen, die auf eine offene Karte erfolgen. Alle anderen werden weggeworfen.
•
Der Tablebaizeview schaut sich die größere Struktur an und akzpetiert keine Drags auf den Depot- bzw. Targetstapel.
•
Das Delegate entscheidet dann anhand der konkreten Karten, ob ein solcher Drag den Spielregeln entspricht.
AUFGEPASST Natürlich wäre es möglich, alle diese Entscheidungen alleine dem Delegate zu überlassen. Aber wir wollen hier ja demonstrieren, wie verschiedene Belange der beteiligten Partner implementiert werden.
359
SmartBooks
Objective-C und Cocoa – Band 2
Zunächst muss CardStackView.m TableBaizeView.h importieren, damit es die dortige Methode ausführen kann: #import "CardStackView.h" #import "TableBaizeView.h"
Wir errechnen in CardStackView nur noch den Kartenindex und zeichnen das Dragging-Image. Den Rest überlassen wir der Instanz von TableBaizeView. Außerdem müssen wir jedoch die Abfragen stehen lassen, ob das Dragging vom Standpunkt des Cardstackviews aus zulässig ist. - (void)mouseDragged:(NSEvent*)event { … // Falls zugedeckte Karte: Weg! if( index < self.countOfNonDisclosedCards ) { return; } // Auch der TableBaizeView muss das Dragging erlauben. // Dies befragt wiederum sein Delegate. TableBaizeView* tableBaizeView = (TableBaizeView*)[self superview]; BOOL allowed = [tableBaizeView allowsDraggingFromCardStackView:self atIndex:index]; if( !allowed ) { return; } …
Das Zeichnen lassen wir freilich bestehen, da dies ja nur der Stapelview wissen kann. Freilich könnte man es ähnlich wie bei einem Tableview machen und auch das Bild vom Delegate abholen. In unserem Falle weiß aber der View mehr über das Aussehen der Karten als das Delegate. Damit das Ganze funktioniert, muss natürlich das Tablebaizeview die entsprechende Methode implementieren und publik machem. Mit Letzterem fangen wir an: @interface TableBaizeView : NSView { … }
360
Kapitel 3
View-Controller-Bindung
… - (BOOL)allowsDraggingFromCardStackView:(CardStackView*)view atIndex:(NSUInteger)index; @end
Die entsprechende Implementierung lassen wir zunächst sehr einfach: - (BOOL)allowsDraggingFromCardStackView:(CardStackView*)view atIndex:(NSUInteger)index { if( view == targetView ) { return NO; } else if( view == depotView ) { return NO; } return YES; } - (id)initWithFrame:(NSRect)frame {
Wie in der Graphik dargestellt, verbieten wir also hier das Dragging von anderen als den Spielerstapeln. Bleibt der letzte Schritt: Wir befragen das Delegate, was es davon hält. Hier müssen wir allerdings aufpassen, da wir nicht wissen, ob die entsprechende Methode überhaupt implementiert ist. Wenn dies der Fall ist, holen wir uns die Antwort ab. Zunächst bauen wir uns dazu eine spezielle Zugriffsmethode am Anfang der Implementierung von TableBaizeView und ändern den Code entsprechend: - (NSUInteger)indexOfView:(CardStackView*)view { if( view == depotView ) { return depotStackIndex; } else if( view == targetView ) { return targetStackIndex; } else { NSUInteger index = [playerViews indexOfObject:view]; if( index != NSNotFound ) { return index; } }
361
SmartBooks
Objective-C und Cocoa – Band 2
return NSNotFound; } - (CardStackView*)viewWithStackIndex:(NSInteger)index … - (BOOL)allowsDraggingFromCardStackView:(CardStackView*)view atIndex:(NSUInteger)index … NSUInteger stackIndex; stackIndex = [self indexOfView:view]; if( stackIndex == NSNotFound ) { return NO; } // Haben wir kein Delegate, so erlauben wir das Ziehen if( [self delegate] == nil ) { return YES; } // Haben wir eines, so befragen wir es SEL selector = @selector( tableBaizeView: shouldDragCardsAtIndex: fromStackWithIndex: ); id myDelegate = self.delegate; if( [myDelegate respondsToSelector:selector] ) { return [delegate tableBaizeView:self shouldDragCardsAtIndex:index fromStackWithIndex:stackIndex]; } return YES; }
Sie können das jetzt bereits probieren. Denn dadurch, dass wir die Delegatemethode noch nicht programmiert haben, muss das Ziehen einzelner Karten weiter funktionieren. Sie können sich vor dem letzten return auch gerne mal einen NSLog einbauen. Delegatemethode implementieren Kommen wir zum letzten Schritt, den Sie strukturell bereits aus Band 1 kennen, und implementieren wir die entsprechende Methode im Delegate. Also ab zu AppDelegate.m und folgenden Code einfügen:
362
Kapitel 3
View-Controller-Bindung
@synthesize filterControllers; - (BOOL)
tableBaizeView:(TableBaizeView*)view shouldDragCardsAtIndex:(NSUInteger)cardIndex fromStackWithIndex:(NSUInteger)stackIndex
{ // Hole Stapel // Es muss sich um einen Spielerstapel handeln stackIndex += playerStacksMinIndex; CardStack* stack = [[self stacks] objectAtIndex:stackIndex]; // Alle Karten ab der gezogenen müssen aufeinander folgen Card* card = [[stack cards] objectAtIndex:cardIndex]; NSNumber* suit = card.suit; NSUInteger value = card.value; for( cardIndex++; cardIndex < [stack.cards count]; cardIndex++ ) { value--; card = [[stack cards] objectAtIndex:cardIndex]; if( ![card.suit isEqualToNumber:suit] || (card.value != value) ) { break; } } // Schleife durchlaufen? return (cardIndex == [stack.cards count]); }
Diese Methode schaut also nach, ob alle noch vorhandenen folgenden Karten über dieselbe Farbe verfügen und in ihrem Wert absteigend sind. Ist dies der Fall, wird die For-Schleife komplett durchlaufen und cardIndex erreicht den Endwert. Dies führt zu YES als Rückgabewert. Da diese Bedingung bei Programmstart stets erfüllt ist – es gibt ja jeweils nur eine offene Karte –, sollten Sie jetzt problemlos Karten ziehen können. Sie können mal testweise den Rückgabewert auf NO setzen und so testen, dass sich dann die Karte auf dem Spielerstapel nicht mehr ziehen lässt. Es sollte auch so sein, dass nach einem Klick auf den Depotstapel sich auf dem vierten bzw. fünften Spielerstapel von links zwei Karten ziehen lassen sollten. 363
SmartBooks
Objective-C und Cocoa – Band 2
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 70« von der Webseite herunterladen.
Pasteboard füllen Wir sind aber immer noch nicht einmal mit dem Beginn des Drag-Zyklusses fertig. Denn natürlich muss das Pasteboard noch vom Delegate befüllt werden. Dies nehmen wir zur Gelegenheit, eine weitere mögliche Struktur für die Kommunikation einer Viewhierarchie mit dem Delegate zu demonstrieren. TableBaizeView delegate … mouseDragged
CardStackView superview -mouseDragged: …
tableBaizeView: shouldWriteCardsAtIndex: fromStackWithIndex: toPasteboard:
Delegate …
Ein (verborgenes) Subview kann auch unmittelbar mit dem Delegate kommnizieren.
Diesmal lassen wir den CardStackView selbst mit dem Delegate kommunizieren. Damit dies funktioniert, muss der im Hintergrund liegende Cardstackview sein Tablebaizeview ermitteln und als Parameter weiterreichen. Bei dieser Struktur liegt das Problem darin, dass der einzelne View nicht weiß, wer er ist. Anders als beim Start des Draggings muss er jedoch nicht die Entscheidung treffen, ob von ihm aus die Operation überhaupt zulässig ist. Dies ist ja bereits bestätigt. Es geht also nur darum, dass er seinen Index erfährt. Hierzu publizieren wir zunächst die entsprechende Methode der Klasse TableBaizeViews in deren Header: @interface TableBaizeView : NSView { … } @property( assign ) id delegate; - (NSUInteger)indexOfView:(CardStackView*)view;
BEISPIEL Natürlich könnte man sich auch hier die Sache erleichtern, indem man dem Stapelviews einen Index mitliefert oder die Eigenschaft tag von Views zum Schreiben implementiert. Dann wäre es möglich, die Methode -tag und -viewWithTag: (jeweils NSView) zu verwenden.
364
Kapitel 3
View-Controller-Bindung
- (void)mouseDragged:(NSEvent*)event { … [dragImage unlockFocus]; NSPasteboard* pasteboard = [NSPasteboard pasteboardWithName:NSDragPboard]; // Pasteboard erstellen und befuellen tableBaizeView = (TableBaizeView*)self.superview; NSUInteger stackIndex = [tableBaizeView indexOfView:self]; SEL selector = @selector( tableBaizeView: writeCardsAtIndex: fromStackWithIndex: toPasteboard: ); id delegate = tableBaizeView.delegate; if( [delegate respondsToSelector:selector] ) { [delegate tableBaizeView:tableBaizeView writeCardsAtIndex:index fromStackWithIndex:stackIndex toPasteboard:pasteboard]; } // Gezogene Karten ausblenden self.draggedCardsCount = self.cardsCount - index; … }
Der strukturell wichtige Punkt ist hierbei, dass auch bei dieser Anordnung das Subview seine Existenz nicht preisgibt, sondern als Parameter den Superview – also die Tablebaizeview – übergibt. Wir behalten damit die Subviews geheim.
GRUNDLAGEN Hierbei kommt zum Tragen, dass in Objective-C zwar der Empfänger (self) und der Selektor (_cmd) als unsichtbare Parameter einer Methode geliefert werden, jedoch nicht der Absender. Es ist vom Standpunkt der objektorientierten Programmierung aus auch merkwürdig, dies bekannt zu machen: Ein Objekt soll auf eine Nachricht auf seinen Daten arbeiten. Von wem die Nachricht stammt, interessiert dabei erst einmal nicht. Muss eine Rückverbindung zum Absender bestehen, so kann dies wie hier über einen Parameter erfolgen.
365
SmartBooks
Objective-C und Cocoa – Band 2
Implementieren wir die entspechende Delegatemethode in AppDelegate.m, nachdem wir zunächst eine Konstante für den Typen definieren: static NSString* const dragTypeKey = @"dragTypeKey"; @implementation AppDelegate … - (BOOL) tableBaizeView:(TableBaizeView*)view shouldDragCardsAtIndex:(NSUInteger)cardIndex fromStackWithIndex:(NSUInteger)stackIndex { … } - (BOOL)
tableBaizeView:(TableBaizeView*)view writeCardsAtIndex:(NSUInteger)cardIndex fromStackWithIndex:(NSUInteger)stackIndex toPasteboard:(NSPasteboard*)pasteboard
{ // Hole Stapel und Karte aus dem Model CardStack* stack = [self.stacks objectAtIndex:stackIndex]; // Schreibe die Daten in das Pasteboard NSArray* dragTypes = [NSArray arrayWithObject:@"Cards"]; [pasteboard declareTypes:dragTypes owner:nil]; NSMutableArray* cards = [NSMutableArray array]; while( cardIndex < [stack.cards count] ) { Card* card = [stack.cards objectAtIndex:cardIndex]; NSNumber* value = [NSNumber numberWithUnsignedInteger:card.value]; NSDictionary* cardDictionary = [NSDictionary dictionaryWithObjectsAndKeys: card.suit, @"suit", value, @"value", nil]; [cards addObject:cardDictionary]; cardIndex++; } NSLog( @"cards:\n%@", cards ); [pasteboard setPropertyList:cards forType:dragTypeKey];
366
Kapitel 3
View-Controller-Bindung
return YES; }
Das ist ja eigentlich Kram, den wir schon in Band I (S. 554 ff.) besprochen hatten.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 71« von der Webseite hertunterladen.
Drag, Drag-Over und Drop Damit haben wir eigentlich die wichtigsten Sachen kennengelernt. Sie können prinzipiell Delegating implementieren (Klick) und mit den verschiedenen Entscheidungsebenen umgehen (Drag starten). Zuletzt implementieren wir jetzt noch das Ziehen auf den neuen Stapel und das Drop. So viel Neues kommt jetzt allerdings nicht mehr. Kümmern wir uns um die Implementierung der Dragging-Source in CardStackView. Die Methoden sollten eigentlich noch vorhanden sein, werden von uns jetzt aber deutlich vereinfacht und an die konkreten Bedürfnisse des Spieles angepasst: - (NSDragOperation) draggingSourceOperationMaskForLocal:(BOOL)local { if( local ) { return NSDragOperationMove; } else { return NSDragOperationNone; } }
Nach den Ausführungen im letzten Kapitel sollten Sie den Code verstehen. Hier ist es wieder interessant, dass bereits der View die entsprechenden Entscheidungen trifft, ohne das Delegate zu befragen. Die Karten sollen ja nur innerhalb des Spielfeldes verschoben werden. Das ist anders als beim Tableview, bei dem das Delegate bestimmen kann, welche Drag-&-Drop-Operationen zulässig sind: Anderer Fall, andere Lösung. Als Erstes müssen wir aber das von uns verwendete Format für die einzelnen Stapel registrieren, damit wir dort überhaupt eine Nachricht erhalten. Hier taucht wieder das Problem der Aufteilung der Verantwortung zwischen View und Delegate auf, denn nur das Delegate kennt das von ihm verwendete Format. Registriert wer367
SmartBooks
Objective-C und Cocoa – Band 2
den muss es allerdings für die einzelnen Spielerstapel, die dem Delegate verborgen sind. Diese Weiterleitung implementieren wir allerdings besonders geschickt: Unser TableBaizeView hat ja bereits von NSView die Methode -registerForDraggedTypes: geerbt. Wir müssen diese also nur überschreiben, um die Registrierung für die Subviews vorzunehmen. Das machen wir in TableBaizeView.m: - (void)registerForDraggedTypes:(NSArray*)types { NSEnumerator* playerViewsEnum = [playerViews objectEnumerator]; for( CardStackView* playerView in playerViewsEnum ) { [playerView registerForDraggedTypes:types]; } } - (id)initWithFrame:(NSRect)frame {
Schicken wir also im Delegate, das ja seinen Drag-Typen kennt, eine entsprechende Nachricht an den Tablebaizeview. In AppDelegate.m: - (void)awakeFromNib … NSArray* dragTypes = [NSArray arrayWithObject:dragTypeKey]; [tableBaizeView registerForDraggedTypes:dragTypes]; }
Kommen wir zur eigentlichen Aufgabe vom Delegate, die Erlaubnis abzuholen, eine Karte abzulegen. Dazu müssen zunächst die entsprechenden Draggingmethoden im Cardstackview implementiert sein. - (NSDragOperation)draggingEntered:(id)info { TableBaizeView* tableBaizeView; tableBaizeView = (TableBaizeView*)self.superview; NSUInteger stackIndex = [tableBaizeView indexOfView:self]; BOOL allowed = YES; SEL selector = @selector(
tableBaizeView: shouldValidateDrop: onStackWithIndex: ); id delegate = tableBaizeView.delegate;
368
Kapitel 3
View-Controller-Bindung
if( [delegate respondsToSelector:selector] ) { allowed = [delegate tableBaizeView:tableBaizeView shouldValidateDrop:info onStackWithIndex:stackIndex]; } if( !allowed ) { return NSDragOperationNone; } self.selectedCardsIndices = [NSIndexSet indexSetWithIndex:self.cardsCount-1]; [self setNeedsDisplay:YES]; return NSDragOperationMove; }
Entsprechend die Delegatemethode im Delegate (AppDelegate.m) nach den bisherigen Delegatemethoden: - (BOOL)
tableBaizeView:(TableBaizeView*)view shouldValidateDrop:(id)dragInfo onStackWithIndex:(NSUInteger)stackIndex
{ NSPasteboard* pasteboard = [dragInfo draggingPasteboard]; NSArray* cards = [pasteboard propertyListForType:dragTypeKey]; NSDictionary* dragCard = [cards objectAtIndex:0]; NSUInteger dragValue = [[dragCard objectForKey:@"value"] unsignedIntegerValue]; CardStack* stack = [self.stacks objectAtIndex:stackIndex]; Card* dropCard = [stack.cards lastObject]; // Auf ein leeres Feld darf jede Karte gelegt werden if( dropCard == nil ) { return YES; } return (dragValue+1) == dropCard.value; }
369
SmartBooks
Objective-C und Cocoa – Band 2
Das System sollte jetzt so langsam verstanden sein. Sie können schon einmal testen, ob nunmehr Karten sich nur noch auf diejenigen Stapel ziehen lassen, deren oberste Karte um eines höher ist. Es spielt übrigens dabei keine Rolle, ob die Karte, an die angelegt wird, dieselbe Farbe hat.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 72« von der Webseite herunterladen. Kommen wir zum letzten Punkt, der das Dragging mit dem Drop abschließt. Zu beachten ist hierbei, dass dies zwei Operationen betrifft: Auf der Dropseite müssen die Karten eingefügt, auf der Dragseite entnommen werden. Da es strukturell wenig Neues gibt, hier in aller Kürze zunächst dropseitig. In CardStackView.m: - (BOOL)performDragOperation:(id)info { TableBaizeView* tableBaizeView; tableBaizeView = (TableBaizeView*)self.superview; NSUInteger stackIndex = [tableBaizeView indexOfView:self]; id delegate = tableBaizeView.delegate; SEL selector = @selector(
tableBaizeView: performDrop: onStackWithIndex: );
if( [delegate respondsToSelector:selector] ) { [delegate tableBaizeView:tableBaizeView performDrop:info onStackWithIndex:stackIndex]; } return NSDragOperationMove; }
Und in AppDelegate.m: - (void)
tableBaizeView:(TableBaizeView*)view performDrop:(id)dragInfo onStackWithIndex:(NSUInteger)stackIndex
{
370
Kapitel 3
View-Controller-Bindung
NSPasteboard* pasteboard = [dragInfo draggingPasteboard]; NSArray* dragCards = [pasteboard propertyListForType:dragTypeKey]; CardStack* stack = [self.stacks objectAtIndex:stackIndex]; NSMutableArray* cards = [stack mutableArrayValueForKey:@"cards"]; for( NSDictionary* dragCard in dragCards ) { Card* card = [[[Card alloc] init] autorelease]; card.disclosed = YES; card.suit = [dragCard objectForKey:@"suit"]; NSUInteger dragValue = [[dragCard objectForKey:@"value"] unsignedIntegerValue]; card.value = dragValue; [cards addObject:card]; } }
Zu beachten ist hier lediglich, dass wir die Karten wirklich aus dem Pasteboard erzeugen.
TIPP Natürlich wäre es in unserem konkreten Falle ohne Weiteres möglich gewesen, lediglich Stapel- und Kartenindex der Quelle in das Pasteboard zu schreiben und dann im Delegate eine entsprechende Verschiebeoperation im Model durchzuführen. Der Standardfall ist es jedoch, dass die Daten wirklich aus dem Pasteboard entnommen werden. Und wir wollten den Standardfall zeigen, nicht konkret mögliche Optimierungen. Sie müssen, wenn noch vorhanden, unbedingt die Methode -draggingUpdate: (CardStackView) entfernen, damit -draggingEntered: die letzte Aussage des Views zur Möglichkeit des Draggings bleibt. Der Drag-&-Drop-Server merkt sich immer die letzte Antwort. Wenn Sie das testen, werden Sie jedoch bemerken, dass natürlich die Karten vom Quellstapel noch nicht entfernt werden. Also als letzten Schritt im CardStackView.m: - (void)draggedImage:(NSImage*)image endedAt:(NSPoint)aPoint operation:(NSDragOperation)operation
371
SmartBooks
Objective-C und Cocoa – Band 2
{ if( operation == NSDragOperationNone ) { return; } TableBaizeView* tableBaizeView; tableBaizeView = (TableBaizeView*)self.superview; NSUInteger stackIndex = [tableBaizeView indexOfView:self]; NSUInteger cardIndex; cardIndex = self.cardsCount - self.draggedCardsCount; id delegate = tableBaizeView.delegate; SEL selector = @selector(
tableBaizeView: removeCardsAtIndex: onStackWithIndex: );
if( [delegate respondsToSelector:selector] ) { [delegate tableBaizeView:tableBaizeView removeCardsAtIndex:cardIndex onStackWithIndex:stackIndex]; } } - (NSDragOperation)draggingEntered:(id)info
Und im Delegate als letzte Delegatemethode in diesem Abschnitt: - (void)
tableBaizeView:(TableBaizeView*)view removeCardsAtIndex:(NSUInteger)cardIndex onStackWithIndex:(NSUInteger)stackIndex
{ CardStack* stack = [self.stacks objectAtIndex:stackIndex]; NSMutableArray* cards = [stack mutableArrayValueForKey:@"cards"]; NSRange removeRange; removeRange.location = cardIndex; removeRange.length = [cards count] - cardIndex; [cards removeObjectsInRange:removeRange]; [[cards lastObject] setDisclosed:YES]; }
372
Kapitel 3
View-Controller-Bindung
HILFE Sie können das Projekt in diesem Zustand als Projekt »Card Game 73« von der Webseite herunterladen. Das Spiel ist jetzt übrigens noch nicht fertig programmiert, weil ein vollständiger Satz auf den Zielstapel verschoben werden können muss. Aber das ist jetzt wirklich mit keinem Erkenntnisgewinn mehr verbunden, so dass wir uns das hier sparen. Wenn Sie wollen …
Zusammenfassung Die Anbindung von eigenen Views an den Controller wurde ausführlich besprochen. Wir haben ganz bewusst wieder nicht den einfachen Weg gewählt und von vorneherein Model und Views passgenau geschneidert. Wenn man ein solches Kartenspiel in der Realität programmiert, geht das zwar häufig noch. Aber schon dann, wenn ein Teil, etwa die Darstellung der Karten, als externes Framework »hinzugekauft« wurde, gelangt man in die Probleme, die wir hier angesprochen haben. Auch unser zweistöckiger Aufbau der Views und das dringende Bedürfnis, die Stapelviews geheim zu halten, hat uns einige Probleme besorgt. Wir konnten diese aber lösen. Und das war das Ziel: Ihnen auch bei komplizierteren Strukturen die verschiedenen Möglichkeiten des Aufbaues zu verdeutlichen. Und bei Bindings oder Delegating gelangen Sie eines Tages an solche Probleme. Sie sollten ein paar Dinge mitnehmen: Data-Source • Sie haben gelernt, wie man Protokolle definiert und verwendet.
•
Hieraus lernten Sie, wie man auch an Instanzen der »Klasse« id Erwartungen stellen darf.
•
Durch die Definition einer Data-Source-API konnten wir die Datenstruktur im Model und im View voneinander unabhängig halten. Gleiches gilt für die Typisierung der einzelnen Informationen.
•
Es war ebenso möglich, dass der auf den verschiedenen Schichten gehaltene Vorrat an Daten sich unabhängig entwickelte. Es besteht die Möglichkeit, einzelne Entitäten auszulagern und erst bei Bedarf anzufordern.
•
Dies reduzierte auch gleich den Nachrichtenverkehr zwischen den Schichten.
373
SmartBooks
Objective-C und Cocoa – Band 2
Bindings • Sie können jetzt zwischen der eigentlichen Synchronisierungstechnologie KeyValue-Observing und der API zum Controller – Bindings – unterscheiden.
•
Wir haben für die veschiedensten Fälle und Strukturen jeweils Observierungen eingerichtet und behandelt. Wir hatten Beispiele für Sets und Arrays.
•
Die möglichen KVO-Optionen und ihre Auswirkungen wurden besprochen. Sie konnten sehen, welche Daten in welchen Fällen mitgeteilt werden.
•
Über Bindings haben wir eine API angeboten, die die Schnittstelle zu unseren Views sauber definierte. Sie haben auch den Umgang mit komplexeren Fragen wie Mehrfachbindings gesehen.
•
Value-Transformer und Prädikate wurden von uns in der Bindingskette eingesetzt.
•
Wir haben sogar einen Array-Controller abgeleitet und diesem eine neue Eigenschaft verpasst, die sich observieren ließ.
Delegating • Wir mussten die Regeln unseres Kartenspieles anpassen. Dazu haben wir allerdings nicht Subclassing, sondern Delegating gewählt.
•
Wir haben auch hier mit Protokollen eine saubere API festgelegt. Der Unterschied zu Data-Sourcen bestand in den optionalen Methoden.
•
Daher sahen Sie, wie man zur Laufzeit nach implementierten Methoden fragt, und haben den sinnvollen Einsatz kennengelernt.
•
Schließlich bedeutet Delegating die Aufteilung von Verantwortung. Dies haben wir an einem Beispiel auf den verschiedensten Ebenen gesehen.
Dieses Kapitel war nicht nur das längste, sondern sicherlich auch das schwierigste. Allerdings sind die hier besprochenen Technologien zentral, so dass uns ihre tiefgehende Besprechung wichtig war. Auch wenn Sie noch nicht alles verstanden – und ganz sicher noch nicht in Ihrem Langzeitgedächtnis abgespeichert – haben, sollten Sie die Grundzüge vormerken. Es handelt sich um Konzepte und Strukturen, die Sie im weiteren Verlauf Ihres Entwicklerlebens immer wieder benötigen werden.
374
Windows, Controls und Cells
4
Nachdem Sie eigene Views programmiert haben, kennen Sie die Welt der Darstellung von innen. Manchmal will man allerdings gar nicht komplett eigene Views haben, sondern lediglich bestehende Elemente der Viewschicht ein wenig anpassen. Mit den wichtigsten Fällen befassen wir uns in diesem Kapitel. Dazu gepackt haben wir noch Tricks mit Fenstern, da es auch hier darum geht, eine bereits vorhandene Funktionalität punktuell an unsere Bedürfnisse anzupassen.
SmartBooks
Objective-C und Cocoa – Band 2
Windows, Controls und Cells In diesem Kapitel wollen wir besprechen, wie man bereits vorhandene Klassen der Viewschicht nur graduell an seine Bedürfnisse anpasst. Sozusagen die kleine Lösung. Das bisher erworbene Grundwissen ist dafür sehr dienlich. Sie werden damit die einzelnen Mechanismen besser verstehen. Zunächst kümmern wir uns um Controls und Cells. Dann werden wir beispielhaft ein Tableview anpassen, indem wir eine Löschtaste implementieren. Schließlich schauen wir uns an, was man mit Fenstern machen kann.
Controls und Cells Während unmittelbare Subklassen von NSView in der Regel komplexe Zeichenund Eingabemöglichkeiten bieten – das haben Sie ja nun in den letzten drei Kapiteln zur Genüge gelernt –, sind Subklassen von NSControl eher dazu gedacht, Werte anzuzeigen. Sie bieten insoweit bereits ausgeprägte Funktionalität, die wir mitnehmen können. Subklassen von NSControl fertigt man vor allem dann an, wenn man das Aussehen eines Controls ändern will. Screw-Control
39 Inhalt
Screw-Control
Inhalt 39
Wir werden einen zusammenklappbaren Control bauen.
Cells sind die Arbeitstiere der Controls. Sie übernehmen die Ausgabe auf dem Bildschirm und in gewissem Umfange auch das Eventhandling. Sie sind anders als Views und damit auch anders als Controls nicht einem Ausschnitt auf dem Bildschirm zugeordnet, sondern werden wie Stempel bei Bedarf an die richtige Stelle verschoben. Dies sorgt dafür, dass sie deutlich leichter sind. Gerade ein komplexer Control wie ein Tableview kann auf diese Weise zum einen durch den Austausch von Cells konfigurierbar gemacht werden auf der anderen Seite durch deren Wiederbenutzung leichtgewichtig bleiben. Es ist dabei durchaus möglich, mehrere Cells pro Control zu haben, wenn die Darstellung dies verlangt oder zumindest die Implementierung dadurch vereinfacht wird. Basisklasse für Controls ist NSControl, für Cells NSCell. 376
Kapitel 4
Windows, Controls und Cells
Wir werden im Folgenden einen Control programmieren, der sich ähnlich wie das Info-Panel im Interface Builder zusammenklappen lässt. Der Zustand wird durch eine Schraube symbolisiert werden, die sich herausbewegt, wenn der Control geschlossen wird. Zuletzt stopfen wir das Ganze in einen eigenen View, der die einzelnen Controls verwaltet und aneinander ausrichtet. Legen Sie bitte ein Projekt aus der Vorlage Cocoa Application ohne Unterstützung von Dokumenten und Core Data an und nennen Sie es Stackspector.
Control Zunächst erzeugen wir uns seine Subklasse von NSControl mit dem Namen ScrewDisclosure. Als Vorlage in New File… wählen Sie Objective-C class und ändern im Header die Klasse. Außerdem müssen wir zwei Eigenschaften hinzufügen, von denen sich eine die Größe des geöffneten Views merkt. Insgesamt in ScrewDisclosure.h: @interface ScrewDisclosure : NSControl { CGFloat disclosedHeight; } @property CGFloat disclosedHeight; @property NSInteger state; @end
Im Modul ScrewDisclosure.m erstellen wir die Accessoren und schreiben uns einen -initWithFrame: und -dealloc: @implementation ScrewDisclosure @synthesize disclosedHeight; - (NSInteger)state { return [self.cell state]; } - (void)setState:(NSInteger)value { [self.cell setState:value]; } - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if( self ) { self.disclosedHeight = frame.size.height; NSCell* cell = [[[NSCell alloc]
377
SmartBooks
Objective-C und Cocoa – Band 2
initTextCell:@"123 Cell ist dabei"] autorelease]; [cell setBordered:YES]; self.cell = cell; self.state = NSOnState; cell.alignment = NSLeftTextAlignment; } return self; } - (void) dealloc { self.disclosedHeight = 0.0; [super dealloc]; } @end
Sie sehen hier bei der Eigenschaft state das erste wichtige Grundprinzip bei der Arbeit mit Controls und Cells: Wichtige Eigenschaften sind in der Zelle gespeichert. Sie werden daher dort gesetzt. Falls Sie entsprechende Methoden in der Controlklasse finden, bedeutet dies nur, dass diese durchgereicht werden. Sie sollten sich mal die Dokumentation zu NSCell durchlesen, um zu sehen, welche Eigenschaften existieren. »Bezeled« heißt übrigens, dass der Control vertieft dargestellt wird. Probieren Sie ruhig herum!
AUFGEPASST Beachten Sie dabei aber bitte, dass in der Basisimplementierung nicht unbedingt alle Eigenschaften bereits unterstützt werden und das Ergebnis daher keine Änderung erfährt. Control cell …
Cell isBordered isBezeled textAlignment …
Wichtige Eigenschaften der Controls können einfach in der Cell abgelegt werden.
378
Kapitel 4
Windows, Controls und Cells
Um das Ganze zu veranschaulichen, setzen wir einmal den Titel des Controls im Application-Delegate @class ScrewDisclosure; @interface StackspectorAppDelegate : NSObject { IBOutlet ScrewDisclosure* aminsDisclosure; IBOutlet ScrewDisclosure* christiansDisclosure; } @end
Die Eigenschaft window haben wir entfernt. Dies machen Sie bitte auch in der Implementierung und erweitern -applicationDidFinishLaunching: @implementation StackspectorAppDelegate - (void)applicationDidFinishLaunching:(NSNotification*)notif { [[aminsDisclosure cell] setStringValue:@"Amin"]; [christiansDisclosure setStringValue:@"Christian"]; } @end
Sie sehen den Unterschied im Code? Mal wird der Text der Cell verändert, mal der des Controls. Führt dies zu Unterschieden in der Anzeige – mal vom Text selbst abgesehen? Um das auszuprobieren, wechseln Sie mit einem Doppelklick auf MainMenu.xib in den Interface Builder. Als Erstes entfernen Sie bitte auch die Verbindung des Application-Delegates zum Window im Interface Builder über das Connection-Pane. Ziehen Sie einen Custom View in das anzuzeigende Fenster und setzen Sie im Identity-Pane des Inspectors die Klasse auf ScrewDisclosure. Ziehen Sie außerdem irgendwelche Views nach Ihrem Belieben als Subviews in den Custom-View. Wir haben Bilder von uns verwendet, weil wir eitle Fatzkes sind. Sie müssen allerdings oben etwas Rand lassen, da dort ja die Beschriftung und die Schraube zum Öffnen und Schließen erscheinen. Verdoppeln Sie den Screwdiscloser und verbinden Sie die beiden ersten Outlets des Application-Delegates mit diesen. Sie können freilich die Kopie auch inhaltlich verändern. Das Ganze sollte jetzt bereits kompilieren. Sie werden sehen, dass der Titel des Disclosures auf beide Arten gesetzt werden kann.
379
SmartBooks
Objective-C und Cocoa – Band 2
Control cell stringValue …
Cell stringValue …
Tatsächlich werden die Eigenschaften durchgereicht.
AUFGEPASST Ihnen wird vielleicht schon negativ aufgefallen sein, dass man eigene Views nicht im Interface Builder in ihrer späteren Schönheit betrachten kann. Doch, das ist möglich, wenn wir ein entsprechendes Plug-In für den Interface Builder erstellen. Hier geht es aber nur um Controls und Cells und wie diese zusammenarbeiten. Wenn Sie jetzt das Projekt starten, werden Sie bereits den Text aus dem -application DidFinishLaunching: und die von Ihnen platzierten Views erkennen. Wir werden uns zunächst nur um das Zusammenklappen dieses Controls kümmern. Dazu bieten wir zwei Actionmethoden an, die das bewerkstelligen. Im Header: @interface ScrewDisclosure : NSControl { … - (IBAction)hideContent:(id)sender; - (IBAction)showContent:(id)sender; @end
Und in der Implementierung: - (void)setState:(NSInteger)value { [self.cell setState:value]; } - (IBAction)showContent:(id)sender { NSSize size = self.frame.size; size.height = self.disclosedHeight; [self setFrameSize:size]; } - (IBAction)hideContent:(id)sender {
380
Kapitel 4
Windows, Controls und Cells
NSSize size = self.frame.size; size.height = [self.cell cellSize].height; [self setFrameSize:size]; }
Während die erste Methode keine Geheimnisse verrät, enthält -hideContent: einen wichtigen Hinweis: Eine Cell weiß immer, wie viel Platz sie zum Zeichnen benötigt. Bei der Breite müssen wir dies nicht beachten, da wir diese durch den Screwdis closure bestimmen. Die Höhe ist indessen für uns interessant und wird als Höhe des geschlossenen Disclosures übernommen. Um dies zu testen, ziehen Sie in MainMenu.xib bitte zwei Buttons in das Fenster, die Sie mit den Actionmethoden -hideContent: und -showContent: eines der Disclosure verbinden. Nach einem Start sollte sich jetzt der Disclosure entsprechend verhalten. Beachten Sie wieder, dass das Koordinatensystem unten links seinen Ursprung hat, so dass sich der Screwdisclosure nach unten verkleinert.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Stackspector 1« von der Webseite herunterladen. Es ist eine – sagen wir es vorsichtig – schwierige Angelegenheit, dass es Actionmethoden zum Öffnen und Schließen der Discloure gibt. Diese sollen sich ja nach dem Zustand der Cell richten. Wir haben das nur gemacht, damit Sie die Funktionalität bis hierher testen können. Bauen wir das zuletzt um und verbergen diese Methoden in einer Kategorie. Dazu legen Sie sich eine neue Klasse von der Vorlage Objective-C class mit dem Namen ScrewDisclosureCellSupport an und ändern entsprechend den Header … #import "ScrewDisclosure.h" @interface ScrewDisclosure( ScrewDisclosureCellSupport ) - (void)hideContent; - (void)showContent; @end
… und die Implementierung können Sie aus der Klasse schieben: @implementation ScrewDisclosure( ScrewDisclosureCellSupport ) - (void)showContent {
381
SmartBooks
Objective-C und Cocoa – Band 2
NSSize size = self.frame.size; size.height = self.disclosedHeight; [self setFrameSize:size]; } - (void)hideContent { NSSize size = self.frame.size; size.height = [self.cell cellSize].height; [self setFrameSize:size]; } @end
Achten Sie aber darauf, dass sich die Parametrisierung ändert. Die ursprünglichen Actionmethoden entfernen Sie bitte samt deren Deklaration im Header. Ebenso die Buttons in MainMenu.xib.
Cell Gut, wir haben jetzt einen Control, der mehr oder minder funktioniert. Schön ist anders. Vielmehr wollen wir, dass die erwähnte Schraube erscheint und diese den Status widerspiegelt.
TIPP Buttons erlauben es, zwei Images – für an bzw. aus – darzustellen. Umschaltknöpfe mit eigenem Aussehen lassen sich so manchmal recht einfach zaubern, ohne dass man Subklassen erstellen muss. Wie bereits erwähnt, sind Cells die Stempel der Controls. Sie sorgen also zuallererst für das Aussehen. Das wollen auch wir anpassen. Um also eigene Darstellungen zu ermöglichen, benötigen wir zunächst eine Subklasse von NSCell. Dazu erzeugen wir eine neue mit New File… (Vorlage: Objective-C class | NSObject), die wir ScrewCell nennen, und passen den Header an: @interface ScrewCell : NSCell { } @end
382
Kapitel 4
Windows, Controls und Cells
In der Implementierung fügen wir nur einen Intialisierer hinzu. Cells haben – Sie ahnen, dass gleich die nächste Besonderheit im Anmarsch ist – zwei DesignatedInitializer, nämlich -initTextCell und -initImageCell. Da wir die Basisimplementierung lediglich den Text zeichnen lassen werden und das Bild selbst erstellen, werden wir nur erstere überschreiben. @implementation ScrewCell - (id)initTextCell:(NSString*)title { self = [super initTextCell:title]; if( self ) { [self setBordered:YES]; [self setBackgroundStyle:NSBackgroundStyleLight]; } return self; } @end
Cell anmelden Als nächsten Schritt müssen wir die Cell nutzen. Wir erzeugen also in unserem Control nunmehr nicht eine Instanz der Standardzelle, sondern unserer Ableitung. Dazu ändern wir ScrewDisclosure.m: #import "ScrewCell.h" @implementation ScrewDisclosure … - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if( self ) { ScrewCell* cell = [[[ScrewCell alloc] initTextCell:@"123 Cell ist dabei"] autorelease]; … } return self; }
383
SmartBooks
Objective-C und Cocoa – Band 2
Cells zeichnen Beim Zeichnen von Cells sind drei Bereiche zu unterscheiden, die sich allerdings teilweise gegenseitig bedingen:
• •
Die Umrandung als einfache Umrandung oder Vertiefung
•
Der eigentliche »Wert« als Inhalt.
Der Hintergrund, meist eine einheitliche Fläche, zuweilen aber auch Verläufe zur Darstellung von Vertiefungen und dergleichen oder besonderer Formen.
Für die verschiedenen Bereiche existieren Eigenschaften wie backgroundStyle, bordered usw. Dabei ist es nicht so, dass es drei Methoden gibt, die die drei Aufgaben wahrnehmen. Es ist nicht einmal so, dass diese drei Bereiche unabhängig voneinander mit den Eigenschaften parametrisiert werden können. Vielmehr kann es gegenseitige Beeinflussungen geben. Ist etwa der Background-Style auf dunkel gesetzt, so zeichnet die Standardimplementierung von NSCell den Text automatisch weiß. Der Background-Style beeinflusst also nicht nur den Hintergrund. Hiervon machen zum Beispiel das Tableview oder Pop-Ups Gebrauch, wenn sie Texte in selektierten Zeilen zeichnet, da auf dem recht intensiven Blau als Selektionsfarbe ein schwarzer Text nur schwierig zu lesen wäre. Auch können so Systemeinstellungen berücksichtigt werden. Oder schauen Sie sich mal einen Button an, bei dem Sie die Umrandung ausschalten: Es verschwindet auch der Hintergrund. Gerade Subklassen fragen daher vielfach unterschiedliche Eigenschaften ab, um eine angenehme Darstellung zu ermöglichen. Hier ist häufig einfach Probieren gefragt, wenn Sie einen bestimmten Effekt erzielen wollen. Umgekehrt bedeutet das aber auch für Sie, dass Sie in den gleich zu besprechenden Methoden nicht auf bestimmte Eigenschaften beschränkt sind, sondern aus dem Vollen schöpfen können.
TIPP Häufig ist es auch so, dass der Interface Builder nur gewisse Kombinationen von Einstellungen zulässt. Hier kann eine größere Vielfalt aus dem Code erreicht werden. Aber Vorsicht: Es ist ja kein Jux, dass der Interface Builder das so beschränkt. Wenn Sie daran etwas ändern wollen, dürfte einiges dafür sprechen, dass Sie gerade die Human-Interface-Guidelines deutlich verlassen. Startpunkte der Zeichnerei sind die Methoden -drawCell: (NSControl) und -drawCellInside: (NSControl). Bedenken Sie dabei, dass ein Control mehrere Zellen besitzen kann. Sollten Sie in einer Ableitung von NSControl hiervon Gebrauch machen, müssen Sie die genannte Methode eben mehrfach aufrufen. (Tatsächlich 384
Kapitel 4
Windows, Controls und Cells
verhält es sich bei der Basisimplementierung von NSControl jedoch so, dass der Aufruf unmittelbar aus -drawRect: erfolgt.) Es werden dann wahlweise zwei Methoden aufgerufen:
•
Falls die Cell-Subklasse -drawWithFrame:inView: implementiert, wird diese Methode durchgeführt. Die Implementierung muss entweder die Superimplementierung aufrufen oder selbst -drawInteriorWithFrame:inView: durchführen, damit auch der Inhalt der Zelle gezeichnet wird.
•
Ist die obige Methode nicht implementiert, so wird unmittelbar -drawInterior WithFrame:inView: ausgeführt. (Dies ist bemerkenswert, da die Basisklasse NSControl -drawWithFrame:inView: enthält.)
Dies ist ziemlich verwirrend und unerwartet, weshalb die Flüche über Cells weit durch die Entwicklergemeinde hallen. Letztlich bekommt man aber das gewünschte Ergebnis hin.
AUFGEPASST Wohl aus diesem Grunde funktionieren Cells der Klasse UICell auf dem iPhone anders als die der Klasse NSCell auf dem Mac. Um die Verwirrung gleich zu Beginn des Kapitels zu vervollständigen, sei auf die Parametrisierung hingewiesen:
•
Anhand des frame-Parameters wird bestimmt, wo die Cell gezeichnet werden soll. Dies entspricht eben dem Stempelcharakter der Zelle: Sie hat keinen Frame wie ein View, sondern ihre Methoden bekommen einen als Parameter übergeben, wenn dies erforderlich ist. Dementsprechend enthält der Parameter auch wirklich die Ausmaße der Cell und nicht das Clipping-Rect wie in -drawRect: (NSView).
•
Auch wenn der zweite Parameter auf NSView typisiert ist, so handelt es sich in der Regel um eine Control-Subklasse. (Schauen Sie sich mal in der Dokumentation den Namen der Parametervariablen an …)
Der zweite Parameter beißt sich mit der Eigenschaft control einer Cell, die den dazugehörigen Control enthält. Gleich werden Sie Genaueres erfahren, aber schon hier: Es gibt Situationen, nämlich beim Editieren, da Ihre Cell zwar zu einem Control gehört, jedoch von einem anderen Element gezeichnet wird. Der Hintergrund ist, dass es standardmäßig pro Fenster ein »Editiercontrol« gibt, welches die Cell gleichermaßen übernehmen kann. Für Sie sollte das nicht von Interesse sein. Verwenden Sie den Parameter, wenn die Methode einen hat, ansonsten die Eigenschaft 385
SmartBooks
Objective-C und Cocoa – Band 2
control. Auch hierauf kommen wir noch zurück. Aufgrund des Stempelcharakters der Cells ist es allerdings meist schlicht unbedeutend, welches Control gerade das Zeichnen veranlasst hat. Die Cell merkt sich übrigens beim Zeichnen, zu welchem Control sie »damals« gehörte. Dies findet sich in der Eigenschaft controlView.
Umrandung zeichnen Die erste wichtige Methode zum Zeichnen einer Cell ist -drawWithFrame:inView:. Sie zeichnet zunächst die Umrandung der Cell und ruft dann die gleich zu besprechende Methode -drawInteriorWithFrame:inView: (NSCell) auf. Zum Zeichnen des Inhaltes ist es daher erforderlich, entweder diese Methode nicht zu implementieren oder in ihr die Superimplementierung aufzurufen oder selbst -drawInterior WithFrame:inView: auszuführen. Andernfalls wird der Inhalt nicht gezeichnet. In der Praxis wird diese Methode jedoch selten überschrieben, da das Zeichnen alleine des Randes wegen meist unproblematisch aus der Basisimplementierung übernommen werden kann. Wir werden jedoch einmal testweise die Methode überschreiben: @implementation ScrewCell - (void)drawWithFrame:(NSRect)frame inView:(NSView*)control { if( [self isBordered] ) { NSRect barRect; NSRect contentRect; NSDivideRect( frame, &barRect, &contentRect, [self cellSize].height, NSMaxYEdge ); [[NSColor blackColor] set]; NSFrameRect( barRect ); NSFrameRect( frame ); } [self drawInteriorWithFrame:frame inView:control]; }
Wir rufen nicht die Basisimplementierung von -drawWithFrame:inRect: auf. Das findet sein Bewenden darin, dass diese ohnehin nur wieder das Rechteck zeichnen würde. Dies bedeutet, dass wir selbst -drawInteriorWithFrame:inView: ausführen müssen, siehe oben. 386
Kapitel 4
Windows, Controls und Cells
AUFGEPASST Falls Sie sich übrigens darüber wundern, dass wir zuletzt den gesamten Frame neu zeichnen und nicht nur den Content-Bereich, so denken Sie bitte daran, dass NSFrameRect() das Rechteck um eine halbe Linienbreite verkleinert, damit die Linie vollständig im Rechteck erscheint. Mit einem Seitenblick ebenfalls auf Kapitel 1 möchten wir hier noch anmerken, dass man natürlich an pixelgenaues Zeichnen denken muss. Die Umrandung sollte wirklich einen Pixel dick sein und auf Pixeln liegen. Wir kümmern uns aus Gründen der Einfachheit nicht darum. Wenn Sie wollen …
Inhalte zeichnen Die praktisch wichtigste Methode zum Zeichnen von Cells ist -drawInteriorWithFrame:inView:. Diese zeichnet den Innenraum. Wir haben drei Dinge zu zeichnen:
• • •
Hintergrund Schraube Text
Hintergrund Beginnen wir mit dem Hintergrund. Exemplarisch zeigen wir Ihnen dabei, wie Sie Eigenschaften berücksichtigen können: - (void)drawInteriorWithFrame:(NSRect)frame inView:(NSView*)control { NSRect barRect; NSRect contentRect; NSDivideRect( frame, &barRect, &contentRect, [self cellSize].height, NSMaxYEdge ); barRect = NSInsetRect( barRect, 1.0, 1.0 ); // Zeichne Hintergrund NSColor* backgroundColor; if( self.backgroundStyle == NSBackgroundStyleDark ) { backgroundColor = [NSColor darkGrayColor]; } else {
387
SmartBooks
Objective-C und Cocoa – Band 2
backgroundColor = [NSColor lightGrayColor]; } [backgroundColor setFill]; NSRectFill( barRect ); } - (void)drawWithFrame:(NSRect)frame inView:(NSView*)control
Dies enthält keine weiteren Hürden. Beachten Sie aber bitte, dass die Hintergrundfläche etwas eingerückt sein muss, damit die Umrahmung nicht übermalt wird. Schraube Die Schraube ist an sich nicht komplizierter zu zeichnen, wenn Sie das erste Kapitel aufmerksam durchgearbeitet haben. Sie besteht aus einem Kreis, einem Schlitz und – falls sie herausgedreht ist – einem Schatten. - (void)drawInteriorWithFrame:(NSRect)frame inView:(NSView*)control { … // Elemente NSRect screwRect; NSRect titleRect; NSDivideRect( barRect, &screwRect, &titleRect, barRect.size.height , NSMinXEdge ); // Schraube zeichnen // Kreis [[NSGraphicsContext currentContext] saveGraphicsState]; if( [self state] == NSOnState ) { NSShadow* shadow = [[[NSShadow alloc] init] autorelease]; NSSize offset; offset.width = +NSWidth( screwRect ) / 10.0; offset.height = -NSHeight( screwRect ) / 10.0; [shadow setShadowOffset:offset]; [shadow setShadowBlurRadius:4.0]; [shadow setShadowColor:[NSColor blackColor]]; [shadow set];
388
Kapitel 4
Windows, Controls und Cells
screwRect = NSInsetRect( screwRect, 3.0, 3.0 ); } else { screwRect = NSInsetRect( screwRect, 4.0, 4.0 ); } NSBezierPath* circle; circle = [NSBezierPath bezierPathWithOvalInRect:screwRect]; [[NSColor controlColor] setFill]; [circle fill]; [[NSGraphicsContext currentContext] restoreGraphicsState]; // Schlitz NSBezierPath* line; line = [[[NSBezierPath alloc] init] autorelease]; NSPoint point; point.x = NSMidX( screwRect ); point.y = NSMinY( screwRect ); [line moveToPoint:point]; point.y = NSMaxY( screwRect ); [line lineToPoint:point]; if( self.state == NSOnState ) { NSAffineTransform* transform = [NSAffineTransform transform]; [transform translateXBy:+NSMidX( screwRect ) yBy:+NSMidY( screwRect )]; [transform rotateByDegrees:90.0]; [transform translateXBy:-NSMidX( screwRect ) yBy:-NSMidY( screwRect )]; [line transformUsingAffineTransform:transform]; } [[NSColor blackColor] setStroke]; [line stroke]; }
Der Code ist zwar umfangreicher, jedoch nicht komplexer. Beachten Sie aber bitte ein Detail: Die Drehung des Schlitzes – der runde Schraubenkopf muss nicht gedreht werden – geschieht hier mit einer affinen Transformation. Eigentlich ließe sich das ja einfacher bewerkstelligen, wenn man ihn einfach waagerecht bzw. senkrecht zeichnen würde. Nur: Wir wollen das später animieren. Und bei einer Ani389
SmartBooks
Objective-C und Cocoa – Band 2
mation kann es ja auch Zwischenwerte geben. Dann würde die Berechnung der Linie deutlich komplizierter. Aus diesem Grunde verwenden wir gleich die eingebaute Rotationsmöglichkeit von Cocoa. Titel Soll Text ausgegeben werden, so existieren verschiedene Möglichkeiten: Bezierpfade haben Sie bereits kennengelernt. Eine weitere Möglichkeit ist die Benutzung des Textsystems, welches Sie noch kennenlernen werden. Dies ist allerdings recht komplex in seiner Handhabung. Es ist daher eine gute Idee – damit wären wir bei der dritten Möglichkeit –, die in NSCell eingebaute Funktionalität für Textausgabe zu nutzen. Wir sagen also einfach, dass die Superklasse den Text ausgeben soll: - (void)drawInteriorWithFrame:(NSRect)frame inView:(NSView*)control { … // Titel ausgeben [super drawInteriorWithFrame:titleRect inView:control]; }
Bitte beachten Sie hier nur, dass das Rechteck, in dem der Text ausgegeben werden soll, nicht das an uns übergebene Rechteck ist. Dann würde ja bei linksbündiger Schrift über die Schraube gezeichnet. Hier zeigt sich wieder der Stempelcharakter der Zellen: Nicht sie bestimmen ihre Lage, sondern der Aufrufer. Sie können jetzt bereits das Projekt starten und die Zeichnerei testen. Da bereits eine Baisimplementierung für das Eventhandling existiert, die den State setzt und das Neuzeichnen veranlasst, lässt sich auch bereits die Schraube bedienen. Testen!
HILFE Sie können das Projekt in diesem Zustand als Projekt »Stackspector 1a« von der Webseite herunterladen.
Events abarbeiten Sie werden aber schon bemerkt haben, dass unser schöner Control nicht mehr zusammenklappt. Das liegt daran, dass das Event in der Zelle abgearbeitet wird. Dabei empfängt zwar der View das Ereignis, leitet es aber zur Bearbeitung an die Zelle weiter. Daher kann diese entscheiden, wie mit einem Event umgegangen werden kann. 390
Kapitel 4
Windows, Controls und Cells
Kommunikation der Cell mit dem Control Eine einfache Lösung für dieses Problem, und das zeigt dann auch die Zusammenarbeit, ist es, sein Control über die Änderung des Status zu informieren. Wir könnten dies wiederum mit Delegating und Protokollen erledigen. Da wir dieses Thema jedoch schon abgehandelt haben, machen wir es uns einfach und nutzen die vorhin erzeugte Kategorie. In ScrewCell.m: #import "ScrewDisclosureCellSupport.h" @implementation ScrewCell -(void)setState:(NSInteger)value { if( (value != self.state ) && (self.disclosure != nil) ) { NSView* control = self.controlView; if( [control isKindOfClass:[ScrewDisclosure class]] ) { ScrewDisclosure* disclosure = (ScrewDisclosure*)control; if( value == NSOnState ) { [disclosure showContent]; } else { [disclosure hideContent]; } } } [super setState:value]; }
Jetzt wird auch vielleicht der Vorteil der Kategorie klar. Durch die Loslösung dieser internen Kommunikation zwischen Cell und Control in eine eigene Datei können wir die entsprechenden Methoden zwischen den beiden sichtbar machen – und nach außen verbergen. Ein Nutzer unserer Klasse ScrewDisclosure sieht nicht die Cell. Das ist einfach besser gekapselt. Jetzt sollte sich der View auch wieder schließen.
Kommunikation des Controls mit der Cell Eigentlich ist das jetzt ein bisschen von uns gelogen. Mit der eigentlichen Eventabarbeitung haben wir ja nichts am Hut. Vielmehr wird dies von der Basisimplementierung von NSCell erledigt. Wir erhalten lediglich das Ergebnis als Statusänderung und leiten das an den Control weiter. Das ist sozusagen der Rückweg von der Cell zum Control.
391
SmartBooks
Objective-C und Cocoa – Band 2
Es kann aber sein, dass Sie auch wirklich die Events selbst bearbeiten wollen. Es sei gesagt, dass dies eher selten ist, da man meist nur das Aussehen von Zellen leicht verändern will, um seiner Anwendung einen letzten Schliff zu verpassen. Die eigentliche Funktionalität ist in den Standardklassen bereits gut abgebildet. Da jedoch eine Zelle keinen ihr zugeordneten Platz auf dem Bildschirm hat, kann sie auch keine Events empfangen. Vielmehr werden die wie gewohnt an den View geschickt. Dieser leitet dann die Events aufbereitet an die Zelle weiter. Dies ergibt einen erheblichen Unterschied, wenn es sich um Mouse-Events handelt, da diese ja ortsbezogen sind – und Cells aufgrund ihres Stempelcharakters immer noch keinen Frame haben. (Wir haben das jetzt häufig genug erwähnt. Es ist aber das grundlegende Missverständnis bei der Arbeit mit Cells.) Grundsätzlich existiert eine Hauptmethode -trackMouse:inRect:ofView:until MouseUp: (NSCell) , welche von Ihnen in -mouseDown: (NSView) des Controls nur aufgerufen wird. Diese werden Sie nicht ableiten. Diese Methode übernimmt dann sozusagen die Eventabarbeitung. Dabei ruft sie wiederum drei weitere Methoden von NSCell auf:
• • •
Zu Beginn wird stets -startTrackingAt:inView: aufgerufen. Wird dann die Maus bei gedrückter Maustaste bewegt, so erfolgen wiederholt Aufrufe von -continueTracking:at:inView: Zuletzt kann die Nachricht stopTracking:at:inView:mouseIsUp: durchgeführt werden.
Wir haben auch eine Anwendung für diese Methode. Und zwar verhält es sich bisher so, dass ein Klick irgendwo in den Control den Disclosure schließt. Dies wollen wir ändern, indem wir nach -setState: folgenden Code einfügen: - (BOOL)trackMouse:(NSEvent*)event inRect:(NSRect)frame ofView:(NSView*)control untilMouseUp:(BOOL)mouseUp { NSPoint location = [event locationInWindow]; location = [control convertPoint:location fromView:nil]; NSRect barRect; NSRect contentRect; NSDivideRect( frame, &barRect, &contentRect, [self cellSize].height, NSMaxYEdge );
392
Kapitel 4
Windows, Controls und Cells
if( NSPointInRect( location, barRect ) ) { return [super trackMouse:event inRect:barRect ofView:control untilMouseUp:mouseUp]; } else { return NO; } } …
TIPP Eine andere Möglichkeit ist es freilich, den Content-Bereich und die Titelzeile in verschiedene Subviews eines eigenen Views zu stecken. Dies ist auch eleganter. Man benötigt dann allerdings eine weitere View-Ebene, die wir Ihnen ersparen wollten. Außerdem haben wir so ein Beispiel für das Überschreiben der Methode. Der Wert für den Parameter mouseUp bestimmt hier übrigens, ob eine längere Mausaktion auch außerhalb des Frames enden darf. Sie sollten am Ende dieses Abschnittes mal testweise den Super-Aufruf mit einem YES versehen. Den Unterschied erkennen Sie darin, dass der Control auch dann zusammenklappt, wenn Sie in dem Control die Maustaste drücken und außerhalb wieder loslassen. Ist er indessen auf NO, so werden nur Mausaktionen bewertet, die im View beginnen und enden. Bei uns ist das anders als etwa bei Slidern, freilich recht unerheblich, da der Disclosure ja nur geklickt wird. Übrigens weisen wir noch darauf hin, dass der Super-Aufruf ein anderes Rechteck erhält. Ab diesem Zeitpunkt gilt also nur noch die Titelzeile als »aktive Fläche« des Controls. Kommen wir zur Implementierung des Startes: … - (BOOL)startTrackingAt:(NSPoint)location inView:(NSView*)control { NSLog( @"startTrackingAt: %@", NSStringFromPoint( location ) ); return YES; } …
393
SmartBooks
Objective-C und Cocoa – Band 2
Es sei angemerkt, dass hier, anders als bei der letzten Methode, der Punkt bereits im Koordinatensystem des Views transformiert übergeben wird. Der Rückgabewert bestimmt, ob bei Mausbewegungen die Zelle weiter informiert werden soll. Wir setzen ihn auf YES für unsere Testzwecke. Denn nur dann wird die nächste Methode ausgeführt: … - (BOOL)continueTracking:(NSPoint)lastLocation at:(NSPoint)location inView:(NSView *)control { NSLog( @"continueTracking:at: %@", NSStringFromPoint ( location ) ); return YES; } …
Für die Parameter und den Rückgabewert gilt hier dasselbe. Wird die Maus bei gedrückter Taste außerhalb des Controls bewegt, so sendet die Basisimplementierung von -trackMouse:inRect:ofView:untilMouseUp: allerdings nur dann entsprechende Nachrichten, wenn der Parameter untilMouseUp YES war. Andernfalls muss die Maus erst zurückkehren. Schließlich: … - (void)stopTracking:(NSPoint)lastLocation at:(NSPoint)location inView:(NSView *)control mouseIsUp:(BOOL)flag { NSLog( @"%d", flag ); }
Diese Methode wird in zwei Fällen durchgeführt, wobei ebenfalls entscheidend ist, ob -trackMouse:inRect:ofView:untilMouseUp: mit NO oder YES gestartet wurde. In unserer bisherigen Variante mit NO kommt es dazu, wenn entweder die Maus das ursprünglich übergebene Rechteck verlässt oder die Maustaste innerhalb des Rechteckes losgelassen wird. Im ersten Fall ist der letzte Paramater NO, im anderen YES. Nur YES bedeutet also einen gültigen Klick. Bedenken Sie zudem, dass die Maus wieder ins Rechteck zurückkehren und erneut verlassen kann. -stopTracking:at:inView:mouseUp: wird dann mehrmals aufgerufen, wobei der Parameter mouseUp dann stets NO ist. Wird die Maus außerhalb des Rechteckes losgelassen, so erhalten wir die Nachricht -stopTracking:at:inView:mouseUp: nicht erneut. 394
Kapitel 4
Windows, Controls und Cells
(Wir haben sie freilich erhalten, als die Maus das Rechteck verließ.) Man kann sich also nicht darauf verlassen, dass am Ende einmal flag YES ist, um dann etwa Aufräumarbeiten zu erledigen. Setzen Sie dagegen testweise den letzten Parameter von -trackMouse:inRect:ofView:untilMouseUp: auf YES (einfach den Super-Call modifizieren), so wird -stopTracking:at:inView:mouseUp: nur aufgerufen, wenn die Maustaste losgelassen wurde. Der Parameter mouseUp ist dann freilich immer YES. Eine Benachrichtigung über das Verlassen des Rechteckes wird nicht gesendet. Am besten ist es, Sie probieren das intensiv aus und schauen sich jeweils die Logs an. Dieses Verhalten ist richtig, wenn Sie eine Änderung des Controls bei gedrückter Maustaste erzielen wollen, wie es etwa bei einem Slider der Fall ist.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Stackspector 2« von der Webseite herunterladen. Bitte entfernen Sie wieder die Methoden bis auf -trackMouse:inRect:ofView:untilMouseUp:. Dort stellen Sie bitte wieder sicher, dass der Parameter untilMouseUp an die Superimplementierung weitergeleitet wird.
Mehrere Panes verwalten Runden wir das Projekt ab und bauen um die einzelnen Controls ein View, welches selbige anordnet. Letztlich ist das dann so etwas Ähnliches wie ein Splitview, auch wenn wir natürlich bei Weitem nicht den Funktionsumfang implementieren werden. Erzeugen Sie sich also eine neue von NSView abgeleitete Klasse, die Sie StackView nennen. Im Interface Builder ziehen Sie einen Custom-View in das Fenster und geben ihm seine Klasse im Identity-Pane des Inspectors. Platzieren Sie nun die beiden Controls bündig in dem StackView, so dass kein Platz in diesem frei bleibt. Im Prinzip muss nur bei einer Größenänderung der Controls die Lage der anderen Controls und die Größe des umfassenden StackViews angepasst werden. Dazu muss der StackView über die Änderung von den Controls benachrichtigt werden. Zunächst sortieren wir erst einmal nach dem Laden die Angelegenheit. Es müssen ja nicht alle Entwickler so diszipliniert sein wie Sie:
395
SmartBooks
Objective-C und Cocoa – Band 2
@implementation StackView - (void)awakeFromNib { // subviews sortieren und anordnen CGFloat lowerEdge = 0.0; for( NSView* control in self.subviews ) { NSRect frame = control.frame; frame.origin.x = 0.0; frame.origin.y = lowerEdge; frame.size.width = self.frame.size.width; control.frame = frame; lowerEdge += frame.size.height-1.0; // Autoresizing ist festgelegt: X-verbreiten und Y-unten NSUInteger autoresizingMask; autoresizingMask = NSViewWidthSizable | NSViewMaxYMargin; [control setAutoresizingMask:autoresizingMask]; } NSRect frame = self.frame; frame.size.height = lowerEdge+1.0; self.frame = frame; }
Sie können das -drawRect: übrigens löschen, da dieses View ja ein reiner Container ist. Als Nächstes schreiben wir uns die eigentlich Methode zur Anpassung. Alle Controls, die oberhalb des geänderten Controls liegen, müssen verschoben und die Gesamthöhe des Stackviews muss angepasst werden: @implementation StackView - (void)rearrangeForSubview:(NSView*)control { // Von unten nach oben CGFloat lowerEdge = -1.0; for( NSView* aboveView in self.subviews ) { if( lowerEdge < 0.0 ) { if( aboveView == control ) { lowerEdge = NSMaxY( control.frame ); } } else { NSRect frame = aboveView.frame; frame.origin.y = lowerEdge;
396
Kapitel 4
Windows, Controls und Cells
aboveView.frame = frame; lowerEdge = NSMaxY( frame ); } } NSRect frame = self.frame; frame.size.height = lowerEdge; self.frame = frame; }
Natürlich müssen wir das jetzt noch benutzen. Dazu ändern wir ScrewDisclosure CellSupport.m: - (void)showContent { … if( [self.superview isKindOfClass:[StackView class]] ) { StackView* stackView = (StackView*)self.superview; [stackView rearrangeForSubview:self]; } } - (void)hideContent { … if( [self.superview isKindOfClass:[StackView class]] ) { StackView* stackView = (StackView*)self.superview; [stackView rearrangeForSubview:self]; } }
HILFE Sie können das Projekt in diesem Zustand als Projekt »Stackspector 3« von der Webseite herunterladen. Wenn Sie noch etwas herumspielen wollen, dann können Sie auch noch mehr Subviews in den Stackview stecken. Ebenso sollten Sie einmal den Stackview wiederum in einen Scrollview stopfen und verkleinern. Dieser ließe sich dann auch noch an die richtige Stelle scrollen.
397
SmartBooks
Objective-C und Cocoa – Band 2
Tableviews, -columns und Cells Tableviews – und damit auch die Subklasse NSOutlineView – erlauben es, für bestimmte Spalten eigene Cells zu setzen. Man macht hiervon zuweilen Gebrauch, um die Anwendung schöner zu gestalten. Beliebtes Beispiel sind die sogenannten ����� Badges���������������������������������������������������������������������������������� . Der Unterschied zur vorangegangen Technik liegt darin, dass wir kein selbstdefiniertes Control-Cell-Paar haben, sondern uns in ein fremdes Control einschleichen. Gleichzeitig stellen wir ein neues Element eines Tableviews vor, nämlich die Tablecolumns. Da Tableviews bereits die Anordnung von einer Vielzahl von Elementen übernehmen und auch für Selektion und Scrolling sorgen, ist eine Anpassung besonders wichtig. Hier kann man viel existierende Funktionalität übernehmen.
Columns Ein Strukturierungsmerkmal von Tableviews sind die Columns. Bei Tablecolumns handelt es sich nicht um Views. Sie dienen lediglich der Verwaltung einer einzelnen Spalte und halten dementsprechend wichtige Eigenschaften wie Größenattribute, Sortierung und Bindings für die Spalte. Im Wesentlichen lässt sich das alles im Interface Builder einstellen. An eine Spalte gelangen Sie über ein Outlet – wie wir es gleich machen werden – oder über die Methode -tableColumns bzw. tableColumnWithIdentifier: (beide NSTableView). Der Identifier wird durch eine Instanz von NSString gebildet. Darüber hinaus verwaltet eine Spalte auch zwei Cells: Die für den Kopf (Header-Cell) und die für die Daten (Data-Cell).
Bei einem Tableview und dessen Subklasse Outlineview werden zwei Zellen pro Spalte benötigt.
Wir werden, um ein Gefühl für Spalten zu bekommen, diese hier ein- und ausblenden. Dazu legen Sie sich bitte ein neues Projekt ohne Support für Dokumente und Core Data an und nennen es CustomOutlineView. Outlineviews sind ja Subklassen von Tableviews, beherrschen also deren Reportoire. Hinzu kommen weitere Eigenschaften, die wir gleich benötigen werden. 398
Kapitel 4
Windows, Controls und Cells
AUFGEPASST Columns sind »spaltenorientiert«, das Tableview selbst zeilenorientiert. Daher bietet das Tableview die Möglichkeit, für einzelne Zeilen Cells vom Delegate mittels -tableView:dataCellForTableColumn:row: zu erhalten. Auch die Tablecolumns können besondere Zellen liefern, nämlich in einer Subklasse mit -dataCellForRow:. Erstere Methode wird man eher wählen, wenn der Zellentyp von der gerade in der Zeile angezeigten Entität abhängt, letzteres eher, wenn aus Layout-Gründen ein bestimmter Zelltyp angezeigt werden soll, etwa zur Unterteilung. Öffnen Sie als Erstes in Xcode das Application-Delegate und ändern Sie den Sourcecode: @interface CustomOutlineViewAppDelegate : NSObject< NSApplicationDelegate > { NSWindow *window; IBOutlet NSMenu* menu; IBOutlet NSTableColumn* childrenColumn; IBOutlet NSTableColumn* ageColumn; }
Die Implementierung müssen wir auch anpassen. Man kann über Klicks auf den Header mittels einer Delegatemethode informiert werden. Genau einen solchen Klick wollen wir dazu verwenden, ein Pop-Up-Menü zu öffnen: @implementation CustomOutlineViewAppDelegate @synthesize window; - (void)
outlineView:(NSOutlineView*)view mouseDownInHeaderOfTableColumn:(NSTableColumn*)column
{ NSEvent* event = [[NSApplication sharedApplication] currentEvent]; if( [event modifierFlags] & NSAlternateKeyMask ) { [NSMenu popUpContextMenu:menu withEvent:event forView:view]; } }
399
SmartBooks
Objective-C und Cocoa – Band 2
Delegating kennen Sie, das muss nicht mehr erläutert werden. Allerdings tauchen hier zwei Feinheiten auf, die erwähnenswert sind: Zum einen sehen Sie, wie man ein Kontektmenü öffnet. Zum anderen benötigt dies das einleitende Event. In der Delegatemethode erhalten wir dieses aber nicht. Man kann sich aber glücklicherweise das Event mit -currentEvent vom Dispatcher der Applikation abholen. Öffnen Sie sodann MainMenu.xib im Interface Builder und ziehen Sie ein Outlineview in das Programmfenster, wählen Sie es an und erzeugen Sie eine dritte Spalte. Geben Sie den Spalten den Titel Name, Children bzw. Age. Fügen Sie unterhalb des Outlineviews drei Buttons mit den Beschriftungen Add Item, Add Child bzw. Remove Item ein. In das Hauptfenster ziehen Sie bitte einen Tree-Controller, den Sie auf eine einfache Art konfigurieren: Bei den Key Paths tragen Sie an oberster Stelle bei children einfach children ein. Bei der Rubrik Object Controller darunter achten Sie darauf, dass Class bei Mode ausgewählt und NSMutableDictionary als Class Name eingestellt ist. Außerdem müssen Sie die Option Prepares Content eingeschaltet haben. Man kann auf diese Weise dafür sorgen, dass der Array-Controller sich selbst ein Array anlegt und dieses mit einem Mutable-Dictionary befüllt. Dazu müssen auch noch darunter die Keys ausgefüllt sein. Dies geschieht jetzt aber automatisch, wenn wir an den Tree-Controller binden. Das machen wir nun auch: Die erste Spalte des Outlineviews binden Sie an den Tree-Controller und geben als Model-Key-Path name ein. Die zweite Spalte Children binden Sie ebenso, wobei Sie aber als Key-Path children.@count angeben, um nur die Anzahl der Kinder zu erhalten. Die letzte Spalte binden Sie schließlich an die mit der Eigenschaft age. Wenn Sie zurück zum Info-Pane des Tree-Controllers schalten, sehen Sie, dass sich dort die Pfade wiederfinden. Bei der Children-Spalte schalten Sie bitte zudem Editable bei den Attributes aus. Es ist ja nun wirklich nicht sinnvoll, die Anzahl einzugeben. Die drei vorhin eingefügten Buttons verbinden Sie mit den Actionmethoden add:, addChild: und remove: des Tree-Controllers, damit wir auch Einträge hinzufügen und löschen können. Als Nächstes benötigen wir natürlich ein Menü. Das ziehen Sie sich aus der Library ins Hauptfenster. Löschen Sie den standardmäßig vorhandenen dritten Eintrag und ändern Sie die beiden bestehenden auf Children bzw. Age. Das sind die beiden Spalten, die man ausschalten kann. Es ist ja nicht wirklich lustig, wenn man einem Outlineview die Disclosure-Spalte nimmt, also jene, mit der Zeilen auf- und zugeklappt werden können. Wir könnten natürlich jetzt Actionmethoden implementieren, die die einzelnen Spalten ein- bzw. ausblenden. Wir brauchen das aber nicht, weil man den Check-Status der Menüeinträge gleich binden kann. Dazu ver400
Kapitel 4
Windows, Controls und Cells
wenden Sie deren value-Binding. Das Objekt, an das gebunden wird, ist das Application-Delegate, weil es ja Verweise auf die Spalten hält. Der Schlüsselpfad lautet entsprechend childrenColumn.hidden bzw. ageColumn.hidden. Damit die Checkhaken »richtig herum« angezeigt werden, müssen Sie zudem noch bei den jeweiligen Bindings NSNegateBoolean als Value-Transformer angeben. Verbinden Sie bitte das menu-Outlet des Applikationsdelegates mit dem neuen Menü. Außerdem müssen die beiden Outlets für die Spalten gesetzt werden. Um dies zu bewerkstelligen müssen Sie im Hauptfenster des Nibs auf die hierarchische Ansicht schalten und sich bis zu den Spalten durchhangeln. Zuletzt wählen Sie bitte Outlineview an und machen unser Application-Delegate zum Delegate des Outlineviews. Bitte testen Sie jetzt die Applikation. Über das Pop-Up-Menü lassen sich die beiden Spalten ein- und wieder ausblenden. Soweit, so gut nachgewiesen, dass die Tabellenspalten Eigenschaften der Tabelle kontrollieren.
HILFE Sie können das Projekt in diesem Zustand als Projekt »CustomOutlineView 1« von der Webseite herunterladen.
Column-Cells Wie bereits erwähnt, hängen an jeder Tabellenspalte zwei Zellen. Während die Zelle für die Titelzeile eher selten verändert wird, ist dies bei der Zelle für die Anzeige der Daten schon häufiger der Fall. Eine beliebte Anforderung hierbei sind sogenannte Badges, also die Zahlen in abgerundeten Rechtecken, wie sie etwa bei Mail oder iTunes zur Anzeige der Anzahl von Elementen in einer Liste benutzt werden.
Ziemlich trendy: Badges zur Darstellung einer Anzahl
401
SmartBooks
Objective-C und Cocoa – Band 2
Man kann sich eine solche Cell für ein Tableview ziemlich einfach bauen. Wir werden das allerdings in einer zweiten Stufe verkomplizieren. Fangen wir aber erst einmal vorne an: Ein einfacher Trick besteht darin, eine eigene Spalte im Outline- bzw. Tableview anzulegen, die eine eigene Zelle hat. Dazu erzeugen Sie sich bitte eine neue Datei aus der Vorlage Objectiv-C class und benennen diese wenig phantasievoll als BadgeCell. In BadgeCell.h ändern Sie die Basisklassse von NSObject auf NSTextFieldCell. Überlegen wir uns die Fälle, die bei der Darstellung auftauchen können. In einem normalen Outlineview ist das sogar etwas komplizierter. So gilt etwa für das AquaErscheinungsbild:
• •
Die entsprechende Zeile ist nicht selektiert, hat also weißen Hintergrund.
•
Die entsprechende ist selektiert, aber der View hat nicht den Fokus. Der Hintergrund ist hellgrau.
Die entsprechende Zeile ist selektiert und das View hat den Fokus. Der Hintergrund ist dunkelblau.
Im ersten und im letzten Fall wird uns als Zelle mitgeteilt, dass der Hintergrund hell sei, im mittleren Falle, wenn also ein blauer Selektionsstreifen gemalt wird, dass er hell sei. Bei einem Sidebarview (sourceview) verhält es sich nicht ganz so:
• •
Die Zeile ist nicht selektiert, es erscheint ein weißer Hintergrund.
•
Die Zeile ist selektiert, das Fenster hat den Fokus, jedoch nicht das View: ein leicht aufgehelltes Blau.
•
Die Zeile ist selektiert und der View hat den Fokus: blau.
Die Zeile ist selektiert, das Fenster hat aber nicht den Fokus: Der Hintergrund ist grau.
Im Übrigen verhalten sich wahrlich nicht alle Programme gleich, auch nicht die von Apple. Vergleichen Sie mal Finder, Mail und iTunes. Nun gut, wir wollen unsere Implementierung machen und uns nicht daran stören, dass irgendwer bei Apple einen Malkasten zu Weihnachten bekam. Damit Sie überhaupt etwas Vernünftiges sehen, machen wir allerdings zwei Sachen: Im Outlineview ziehen Sie bitte aus der Library eine Textfield-Cell in die Spalte Children. Klicken Sie sich bis zu dieser durch und setzen Sie dann im Identity-Pane 402
Kapitel 4
Windows, Controls und Cells
unsere Subklasse BadgeCell. Im Size-Pane wählen Sie bitte unter Size den Eintrag Small aus. Außerdem wählen Sie bitte wieder das Outlineview an und geben dort im Size-Pane bei Row Height den Wert 21 ein. So viel Platz muss sein, damit das irgendwie lesbar und erträglich aussieht. Jetzt duplizieren Sie bitte den Outlineview und wählen beim Original im Attributes-Pane bei Highlight den Eintrag Source List. Jetzt können wir später die verschiedenen Situationen behandeln. Ein Grundproblem liegt darin, dass eigentlich alle Farben sich unmittelbar aus dem graphischen Zustand der Cell bzw. ihrer Control ableiten lassen sollten. Bei normalen Tableviews funktioniert das auch wunderbar. Ist bei der Zelle das Attribute highlighted gesetzt, so gibt die Methode -highlightColorWithFrame:inView: die entsprechende Farbe zurück, also für ein Tableview mit Fokus +alternateSelectedControlColor und +secondarySelectedControlColor. Bei Sourceviews funktioniert das nur nicht, was auch einigermaßen verständlich ist. Man mag bedenken, dass die Selektion nicht durch eine Farbe, sondern durch einen Gradient dargestellt wird. Wir müssen daher leider zur Ermittlung der Farbe zu zwei Tricks greifen:
• •
Zum einen ermitteln wir die einzelnen Farben aus dem Bildschirm. Zum anderen benutzen wir Statusattribute, um die richtige Farbe auszuwählen.
TIPP Das Zeichnen der Selektion wird in der Methode -highlightSelectionInClip Rect: (NSTableView) erledigt und kann daher von Ihnen in einer Subklasse angepasst werden. Dabei sollten Sie gleich eine API anbieten, welche Informationen über die gewählte Farbe erlaubt. Wenn man solch »dreckigen« Code hat, der einem eigentlich nicht passt, ist es eine gute Idee, diesen in einer eigenen Methode zu isolieren, wo bei späteren Änderungen leicht eine Anpassung vorgenommen werden kann. Machen wir das also: @implementation BadgeCell - (NSColor*)badgeColorWithFrame:(NSRect)rect inView:(NSView*)control { NSColor* color; if( ![control isKindOfClass:[NSTableView class]] ) { return [super highlightColorWithFrame:rect inView:control]; } …
403
SmartBooks
Objective-C und Cocoa – Band 2
Die Anpassung soll nur für Tableviews erfolgen. In all anderen Fällen wird einfach mit der Super-Nachricht die Standardfarbe für Selektionen verwendet. … NSTableView* tableView = (NSTableView*)control; // "Normales" Tableview if( [tableView selectionHighlightStyle] != NSTableViewSelectionHighlightStyleSourceList ) { if( self.isHighlighted ) { return = [super highlightColorWithFrame:rect inView:control]; } else { color = [NSColor controlTextColor]; } …
Bei einem »normalen« Tableview, welches also kein Sourceview ist, wird einfach für ein selektiertes Element die Selektionsfarbe, ansonsten die Standardfarbe für Text verwendet. … // Sourceview } else { // unselektiertes Element if( !self.isHighlighted ) { color = [NSColor colorWithCalibratedRed:0.61569 green:0.67059 blue:0.79608 alpha:1.0]; …
Hier, bei einem Sourceview, existiert zum ersten Mal keine Standardfarbe. Die gewählte stammt vom Bildschirm. … // selektiert, aber Fenster inaktiv } else { NSWindow* window = control.window; if( !window ) {
404
Kapitel 4
Windows, Controls und Cells
return [super highlightColorWithFrame:rect inView:control]; } …
Der Anfang des »dreckigen« Codes: Es wird nach dem Status des Fensters gefragt. Eigentlich sollte hier bereits ein Fenster existieren, da ansonsten ja nicht gezeichnet werden kann. Sicherheithalber wird dies jedoch abgefragt und notfalls mit der super-Implementierung die Ausführung der Methode abgebrochen. … // Fenster ist inaktiv if( !window.isMainWindow ) { color = [NSColor colorWithCalibratedRed:0.67451 green:0.67451 blue:0.67451 alpha:1.0];
// (Fenster ist aktiv.) control hat keinen Fokus } else if( window.firstResponder != control ) { color = [NSColor colorWithCalibratedRed:0.61569 green:0.67059 blue:0.79608 alpha:1.0]; // (control hat den Fokus) } else { color = [NSColor colorWithCalibratedRed:0.31373 green:0.53333 blue:0.80392 alpha:1.0]; } } } return color; }
Die Implementierung dürfte sich von selbst erklären. Achten Sie nur auf die Abfrage des First-Responder-Status. Bitte beachten Sie noch, dass dies nur zur Auswahl der zweiten Farbe neben Weiß dient, nicht jedoch zur Beantwortung der Frage, ob der Badge invertiert (weiße Schrift, dunkler Hintergrund) ist. 405
SmartBooks
Objective-C und Cocoa – Band 2
Sie können freilich eigene Farben mit einer eigenen Auswahl setzen. Probieren Sie ruhig herum.
TIPP Wenn Sie allerdings möchten, dass sich die Farbe eines unselektierten Badg es ändert, wenn er den Fokus verliert, werden Sie an Grenzen stoßen, da nämlich die Standardimplementierung von NSTableView in diesem Falle nur den Bereich des Selektionsbalkens zum Neuzeichnen markiert. Sie müssen dann also eine Subklasse von NSTableView erzeugen, der den gesamten sichtbaren Teil der Tabelle neu zeichnet.
Der Radius der Halbkreise errechnet sich aus der Höhe.
Eine weitere Hilfsmethode benötigen wir für die Größenberechnung der Zelle. Wie bereits erwähnt, dient die Methode -cellSize der Ermittlung der minimalen Größe. Wir nutzen dies, um die wirkliche Größe der Cell bei einem bestimmten Text zu ermitteln. Ansonsten würde der Badge die gesamte Spalte ausfüllen. Allerdings muss dem Platz für den Text noch der Durchmesser (genauer: zweimal der Radius) hinzuaddiert werden, was sich wiederum aus der Höhe ergibt. @implementation BadgeCell - (NSSize)cellSize { NSSize size = [super cellSize]; size.width += size.height; return size; }
Kommen wir endlich zur eigentlichen Zeichenroutine: - (void)drawInteriorWithFrame:(NSRect)rect inView:(NSView*)control { // Kein Wert oder 0-Wert NSString* count = self.stringValue; if( count && [count isEqualToString:@"0"] ) {
406
Kapitel 4
Windows, Controls und Cells
self.stringValue = @""; count = nil; } if( !count || [count isEqualToString:@""] ) { [super drawInteriorWithFrame:rect inView:control]; return; } …
Hier werden erst einmal diejenigen Fälle aussortiert, in denen kein Badge gezeichnet werden soll, weil der Wert 0 usw. ist. … // Die Groesse auf die minimale Breite setzen und platzieren self.stringValue = @"Wert"; NSSize size = [self cellSize]; switch( self.alignment ) { case NSRightTextAlignment: rect.origin.x += (NSWidth( rect ) - size.width); break; case NSCenterTextAlignment: rect.origin.x += (NSWidth( rect ) - size.width)/2.0; break; case NSLeftTextAlignment: default: break; } rect.origin.y += (NSHeight( rect ) - size.height)/2.0; rect.size = size; NSRect textRect = NSInsetRect( rect, size.height/2.0, 0.0 ); …
Es handelt sich nur um ein paar geometrische Berechnungen. Bitte bemerken Sie, dass das Alignment berücksichtigt wird. … // Dunkler Hintergrund NSColor* darkColor = [self badgeColorWithFrame:rect inView:control];
407
SmartBooks
Objective-C und Cocoa – Band 2
// Unselektiert if( self.isHighlighted == NO ) { [darkColor setFill]; [self setTextColor:[NSColor whiteColor]]; // Selektiert: Farben drehen } else { [[NSColor whiteColor] setFill]; [self setTextColor:darkColor]; } …
Wie bereits ausgeführt, wurde oben nur die zweite Farbe neben Weiß ermittelt. Hier wird über die Zuordnung zu Badge und dessen Text entschieden. Als Kriterium gilt die Eigenschaft highlighted. … NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:size.height/2.0 yRadius:size.height/2.0]; [path fill]; [self setHighlighted:NO]; [super drawInteriorWithFrame:textRect inView:control]; } @end
Zuletzt wird das Badge gemalt. Davor muss das highlighted (Selektion) ausgeschaltet werden, weil sonst die Basisimplementierung in einem Tableview den üblichen Selektionsbalken malen würde. Testen Sie die Applikation. Selektieren Sie beide Tableviews. Legen Sie das Fenster auch einmal in den Hintergrund.
HILFE Sie können das Projekt in diesem Zustand als Projekt »CustomOutlineView 2« von der Webseite herunterladen.
408
Kapitel 4
Windows, Controls und Cells
Kombinierte Cells Unsere Lösung funktioniert schon ganz gut für Sourceviews. Das liegt darin begründet, dass diese in der Regel keine Titelzeile haben, es also gar nicht auffällt, dass es sich in Wahrheit um zwei Spalten handelt. Bei uns geht das aber wegen des Alters nicht und deshalb, weil die Outline-Spalte sich ja verschiebt. Und natürlich ist das Absicht. Denn dahinter steckt ein größeres Problem:
•
Manchmal möchte man zwei Werte unter nur einer Titelzeile anzeigen. Das geht nicht, weil ein Tableview die Kopffelder immer an der Spalte ausrichtet. Das ist ja keine Tabellenkalkulation.
•
Wenn man in einer Aufklappspalte eines Outlineviews neben einem Text ein Symbol anzeigen möchte, funktioniert das nicht mit einer eigenen Spalte. Schauen Sie etwa in die Projektleiste Groups & Files von Xcode: Das Symbol neben den Einträgen wird eingerückt, kann also nicht in eine eigene Spalte gesetzt werden. Es müssen also in einer Spalte eingerückt der Text und das Symbol gezeigt werden.
•
Der breiteste Wert für die Badges würde in jeder Zeile den vollen Platz wegnehmen. Dies ist aber nicht erforderlich und daher Platzverschwendung. Klarer wird das, wenn Sie mal iTunes betrachten:
Die verschiedenen Symbole belegen nicht eine komplette Spalte.
Es ist also zuweilen erforderlich, in einer Spalte zwei Informationen darzustellen, etwa den Namen einer Datei und ihren Typ. Und ein Weg, dies zu machen, ist es, zwei Cells in einer Spalte zu haben. Da eine Spalte jedoch nur eine Data-Cell verwaltet, erledigt man dies, indem man – zumeist – eine Textzelle ableitet und dann hierbei eine weitere hinzufügt. So werden wir das auch machen, indem wir eine TextAndAdditionalCell erzeugen.
409
SmartBooks
Objective-C und Cocoa – Band 2
Die neue Zelle kennt einen Partner.
TIPP Es ist natürlich auch möglich, der Spalte mehrere Data-Cells zu geben. Diese Implementierung ist jedoch nicht einfacher, da wir dann nicht nur die Tabellenspalte ableiten, sondern zugleich in die Zeichenfunktionalität des Tableviews eingreifen müssten. Das Tableview geht aber weitgehend davon aus, pro Spalte nur eine Data-Cell zu haben. Das wird eklig. Kombinierte Zelle erstellen Erzeugen Sie sich bitte eine neue Datei aus der Vorlage Objective-C Class (Basisklasse: NSObject) und nennen Sie diese TextAndAdditionalCell. Den Header passen wir an: @interface TextAndAdditionalCell : NSTextFieldCell { IBOutlet NSCell* additionalCell; } @property( copy ) NSCell* additionalCell; @end
Wir leiten also wie angekündigt von einer Text-Field-Cell ab, um wie vorher die Textausgabe gleich zu importieren. Ein Outlet wird verwendet, um die weitere Cell im Interface Builder setzen zu können. Das vereinfacht die Verwendung. Das Problem bei der Implementierung ist, dass wir einen zweiten Keypath benötigen, wenn wir einen anderen Wert darstellen wollen.
AUFGEPASST Dies wäre etwa nicht der Fall, wenn wir die Anzahl der Zeichen in dem Textfeld in der weiteren Zelle anzeigen oder wir den Text für das Badge aus dem Schlüsselpfad für das Textfeld auf einem anderen Weg ermitteln können. Bei uns könnte man etwa daran denken, die Bindung des Textfeldes zu verwenden und gegebenenfalls einfach den Tree-Controller zu befragen, welche Children-Keypath er verwendet. Aber wir wollen das allgemein halten.
410
Kapitel 4
Windows, Controls und Cells
In der Implementierung müssen wir – da wir eigene Instanzvariablen haben – den Standardkram herunterschreiben: @implementation TextAndAdditionalCell @synthesize additionalCell; - (id)copyWithZone:(NSZone*)zone { TextAndAdditionalCell* copy = [super copyWithZone:zone]; copy->additionalCell = nil; [copy setAdditionalCell:self.additionalCell]; return copy; } …
Gleich hier wird es intellektuell anspruchsvoll: NSCell implementiert das CopyingProtokoll, wovon das Tableview auch tatsächlich Gebrauch macht. Wenn also die Zelle kopiert werden kann, so müssen wir dafür sorgen, dass auch unsere Instanzvariablen kopiert werden (Deep-Copy). Deshalb haben wir eine eigene Implementierung der Methode. Allerdings sieht die doch erstaunlich merkwürdig aus. Und dies hat mal wieder einen Performancegrund, um das außerordentlich zurückhaltend zu formulieren. Damit Cells schnell kopiert werden können, verwendet die Basisimplementierung – räupser – die im Hinblick auf Laufzeitverhalten optimierte Funktion NSCopyObject(). Diese kopiert einfach den kompletten Inhalt des Objektes, ohne sich um irgendwas wie Speicherverwaltung zu kümmern. Nach dem super-Call haben wir daher ein völlig inkonsistentes Objekt, da die Instanzvariablen, die IDs sind, allesamt lediglich mit assign zugewiesen wurden – ohne jede Rücksicht auf retain oder copy. Eine erneute Zuweisung würde daher an die alte Instanz unserer Instanzvariablen ein unmotiviertes und nicht balanciertes retain schicken. Aus diesem Grunde muss das zerstörerische Tun von NSCopyObject() wieder gelöscht werden, und zwar, indem wir unmittelbar in die Instanzvariable einen Leerwert schreiben. Dadurch ändert der folgende Aufruf des Setters nicht mehr den Reference-Count des Originals. Alternativ könnte man anstelle der Zuweisung ein retain an ����� additionalCell�������������������������������������������������������������������� und additionalKeyPath senden, um die Speicherverwaltung wieder auszugleichen. Da wir aber ohnehin sogleich mit dem Setter eine neue Instanz setzen, ist das untunlich. (Wir möchten uns hier noch bei Markus Müller bedanken, der auf die Verwendung von NSCopyObject() hinwies.)
411
SmartBooks
Objective-C und Cocoa – Band 2
NSCopyObject() erhöht trotz Kopie nicht den Reference-Count. Nach einer Zuweisung wird das verwiesene Objekt gelöscht.
… - (void) dealloc { self.additionalCell = nil; [super dealloc]; } @end
Der Rest ist dann glücklicherweise nur noch bekannter Standardkram. Keine Initialisierer? Nein, denn wir könnten ohnehin nur nil vorsetzen. Dies sollte ein Initialisierer aber nicht explizit tun, da +alloc ohnehin alle Werte auf 0 setzt. Kommen wir zur eigentlichen Ausgabe: static const CGFloat spacingRatio = 0.3; @implementation TextAndAdditionalCell @synthesize additionalCell; - (void)
getMainRect:(NSRect*)mainRect getAdditionalRect:(NSRect*)additionalRect forBounds:(NSRect)rect
{ rect = [super titleRectForBounds:rect]; if( self.additionalCell ) { NSSize additionalSize = [self.additionalCell cellSize]; CGFloat shrink = additionalSize.width;
412
Kapitel 4
Windows, Controls und Cells
if( shrink < rect.size.width ) { NSDivideRect( rect, additionalRect, mainRect, shrink, NSMaxXEdge ); NSRect spacingRect; shrink = additionalSize.height * spacingRatio; NSDivideRect( *mainRect, &spacingRect, mainRect, shrink, NSMaxXEdge ); } } else { *mainRect = rect; *additionalRect = NSZeroRect; } } - (void)drawInteriorWithFrame:(NSRect)rect inView:(NSView*)control { NSRect mainRect; NSRect additionalRect; [self getMainRect:&mainRect getAdditionalRect:&additionalRect forBounds:rect]; if( additionalRect.size.width > 0.0 ) { [additionalCell drawInteriorWithFrame:additionalRect inView:control]; } if( mainRect.size.width > 0.0 ) { [super drawInteriorWithFrame:mainRect inView:control]; } }
Man muss nicht viel erläutern. Im Grunde genommen wird in der ersten Methode ermittelt, welche Rechtecke benutzt werden sollen. Beachten Sie, dass im -drawInteriorWithFrame:inView: auch abgefragt wird, ob noch Platz für den Text vorhanden ist. Das Alignment werten wir dieses Mal aus Gründen der Einfachheit nicht aus.
413
SmartBooks
Objective-C und Cocoa – Band 2
Wechseln Sie jetzt wieder in den Interface Builder und ziehen Sie aus der Library eine Instanz von NSTextFieldCell ins Hauptfenster MainMenu.xib. Im Identity-Pane des Inspectors stellen Sie die Klasse BadgeCell ein. Sie sollten auch hier die Control Size auf Small stellen. In die erste Spalte des Outlineviews stellen Sie zudem die Klasse der Zelle auf unsere neue Errungenschaft TextAndAdditionalCell. Verbinden Sie jetzt das Outlet additionalCell dieser Zelle mit der Badge-Cell im Hauptfenster. Wenn Sie die Applikation starten, sollten in der Outlinespalte rechts die Badge-Cells erscheinen.
HILFE Sie können das Projekt in diesem Zustand als Projekt »CustomOutlineView 3« von der Webseite herunterladen. Editieren Wenn Sie allerdings versuchen, den Text in der Outlinespalte zu editieren, bemerken Sie, dass sich dieser rigoros und unter Verdrängung der weiteren Zelle in der gesamten Spalte ausdehnt. Dies ist natürlich nicht schön, lässt sich aber leicht durch zwei zentrale Methoden beseitigen. Diese werden von Cocoa benutzt, um die Ausdehnung für einen solchen Text zu erfragen. Holen wir dies nach: - (void)editWithFrame:(NSRect)rect inView:(NSView*)control editor:(NSText*)editor delegate:(id)delegate event:(NSEvent*)event { NSRect mainRect; NSRect additionalRect; [self getMainRect:&mainRect getAdditionalRect:&additionalRect forBounds:rect]; [super editWithFrame:mainRect inView:control editor:editor delegate:delegate event:event]; }
414
Kapitel 4
Windows, Controls und Cells
- (void)selectWithFrame:(NSRect)rect inView:(NSView*)control editor:(NSText*)editor delegate:(id)delegate start:(NSInteger)start length:(NSInteger)length { NSRect mainRect; NSRect additionalRect; [self getMainRect:&mainRect getAdditionalRect:&additionalRect forBounds:rect]; [super selectWithFrame:mainRect inView:control editor:editor delegate:delegate start:start length:length]; } - (id)copyWithZone:(NSZone*)zone
Wie ersichtlich, liegt der Trick lediglich darin, diese Methoden so zu überschreiben, dass die Super-Implementierung mit einem für unsere Zwecke angepassten Rechteck aufgerufen wird. Testen Sie erneut die Applikation und beachten Sie den Focusring bei der Eingabe.
HILFE Sie können das Projekt in diesem Zustand als Projekt »CustomOutlineView 4« von der Webseite herunterladen. Zweiten Wert setzen Was allerdings noch fehlt, ist die Anzeige des zweiten Wertes. Bisher steht da ja noch der Standardwert. Wir benötigen also eine Möglichkeit, den Wert der weiteren Zelle zu setzen. Nur weiß man in der Methode -drawInteriorWithFrame:inView: in der Regel nicht mehr, zu welcher Spalte man gehört. Die Spalte hält aber das Binding. Es existieren mehrere Möglichkeiten, an die notwendige Information zu kommen:
•
Tableviews kennen die Methode -preparedCellAtColumn:row:, welche die Möglichkeit bietet, für jede Zeile und Spalte zusätzliche Initialisierungen vor415
SmartBooks
Objective-C und Cocoa – Band 2
zunehmen. Hier könnten wir den zweiten Wert setzen. Allerdings müssten wir dazu zusätzlich das Outlineview ableiten.
•
Wir merken uns für die Zelle (oder ihre Spalte) einen zweiten Schlüsselpfad für die zweite Eigenschaft. Diese Möglichkeit hat das Problem, dass der weitere Wert des Objektes hinter der entsprechenden Zeile nicht automatisch gesetzt wird. Üblicherweise erfragt die Zelle ja nicht ihren Wert, sondern bekommt ihn mitgeteilt, bevor ihre Zeichenmethode ausgeführt wird. Um den Rückweg zu finden – und genau darum geht es uns auch in diesem Beispiel – muss die Zelle zunächst das Objekt ermitteln, dessen Eigenschaften sie gerade darstellt.
•
Eine weitere Möglichkeit besteht darin, in den Bindings nur den Grundpfad, also ohne den Model-Key-Path, zu setzen und diesen beim Zeichnen anzuhängen. Hier wird jedoch das Editieren problematisch.
Wir verwenden dabei zu Demonstrationszwecken die zweite Methode, da verschiedene Mechanismen des Outlineviews demonstriert werden können. Dazu ändern Sie bitte TextAndAdditionalCell.h: @interface TextAndAdditionalCell : NSTextFieldCell { IBOutlet NSCell* additionalCell; NSString* additionalKeyPath; } @property( copy ) NSCell* additionalCell; @property( copy ) NSString* additionalKeyPath; @end
und die entsprechende Implementierung: @implementation TextAndAdditionalCell @synthesize additionalCell; @synthesize additionalKeyPath; - (void)setCellValueAndGetMainRect:(NSRect*)mainRect getAdditionalRect:(NSRect*)additionalRect forBounds:(NSRect)rect { rect = [super titleRectForBounds:rect]; if( self.additionalCell && self.additionalKeyPath ) { NSPoint center = NSMakePoint( NSMidX( rect ), NSMidY( rect ) ); NSView* control = self.controlView; if( [control isKindOfClass:[NSOutlineView class]] ) {
416
Kapitel 4
Windows, Controls und Cells
NSOutlineView* outlineView = (NSOutlineView*)control; NSInteger rowIndex = [outlineView rowAtPoint:center]; NSTreeNode* node = [outlineView itemAtRow:rowIndex]; id item = [node representedObject]; id value = [item valueForKeyPath:self.additionalKeyPath]; [additionalCell setObjectValue:value]; } NSSize additionalSize = [self.additionalCell cellSize]; … } else { … } } …
Zunächst mag es verwundern, dass wir den Wert der Zelle in der Methode zur Ermittlung der Rechtecke stecken. Aber überlegen Sie: Die Aufteilung der Rechtecke erfolgt dynamisch abhängig von der Breite der zusätzlichen Zelle. Diese kann aber wiederum von ihrem Inhalt abhängen, was bei uns sogar so ist. Also muss zunächst der Wert gesetzt werden. Wir haben daher auch die Methode umbenannt. Sie ist jetzt die Setzmethode für die weitere Zelle und liefert sozusagen nur noch nebenher die Rechtecke. Wir wollen hier auf den Rückweg zum Model hinaus: Diesen finden wir, indem wir mit rowAtPoint: abfragen, in welcher Zeile wir uns befinden. Mit dieser Information bekommen wir mit itemAtRow: den zugehörigen Eintrag. Dies ist aber nicht unmittelbar unser Model-Objekt, da der Tree-Controller zur Verwaltung Zwischeninstanzen der Klasse NSTreeNode erzeugt. Bedenken Sie, dass ihm die nicht einfache Aufgabe obliegt, eine hierarchische Struktur in dem Outlineview zeilenweise zu »plätten«. Daher müssen wir bei diesem noch anfragen, welches Objekt hinter ihm steht.
Der Tree-Controller erzeugt eine Ersatzstruktur.
417
SmartBooks
Objective-C und Cocoa – Band 2
Übrigens wollen wir nicht verschweigen, dass die Abfrage der Klasse etwas unelegant ist, da wir den Weg verschließen, auch anderen Controls die benutzten Methoden beizubringen. Mindestens sollte ein weiterer Zweig für Tableviews angelegt werden. Wir benötigen diese Möglichkeiten jedoch hier für unser Beispiel nicht. Wir müssen wegen der Änderung des Methodennamens entsprechend unseren bisherigen Code anpassen: … - (void)drawInteriorWithFrame:(NSRect)rect inView:(NSView*)control { NSRect mainRect; NSRect additionalRect; [self setCellValueAndGetMainRect:&mainRect getAdditionalRect:&additionalRect forBounds:rect]; … } - (void)editWithFrame:(NSRect)rect inView:(NSView*)control editor:(NSText*)editor delegate:(id)delegate event:(NSEvent*)event { … [self setCellValueAndGetMainRect:&mainRect getAdditionalRect:&additionalRect forBounds:rect]; … } - (void)selectWithFrame:(NSRect)rect inView:(NSView*)control editor:(NSText*)editor delegate:(id)delegate start:(NSInteger)start length:(NSInteger)length { …
418
Kapitel 4
Windows, Controls und Cells
[self setCellValueAndGetMainRect:&mainRect getAdditionalRect:&additionalRect forBounds:rect]; … } - (id)copyWithZone:(NSZone*)zone { TextAndAdditionalCell* copy = [super copyWithZone:zone]; copy->additionalCell = nil; [copy setAdditionalCell:self.additionalCell]; copy->additionalKeyPath = nil; [copy setAdditionalKeyPath:self.additionalKeyPath]; return copy; } - (void) dealloc { self.additionalKeyPath = nil; self.additionalCell = nil; [super dealloc]; }
Zuletzt muss dieser Schlüsselpfad noch gesetzt werden. Dies erledigen wir im Application-Delegate: #import "TextAndAdditionalCell.h" @implementation CustomOutlineViewApPDelegate … - (void)applicationDidFinishLaunching:(NSNotification*)notif { NSCell* cell = [childrenColumn dataCell]; if( [cell isKindOfClass:[TextAndAdditionalCell class]] ) { TextAndAdditionalCell* combinedCell = (TextAndAdditionalCell*)cell; [combinedCell setAdditionalKeyPath:@"children.@count"]; } }
Setzen Sie nunmehr das Outlet childrenColumn auf die Outline-Spalte Name, da ja jetzt dort unser Badge erscheint. Außerdem löschen Sie bitte in -drawInterior419
SmartBooks
Objective-C und Cocoa – Band 2
WithFrame:inView: (BadgeCell) die Zeile, in der der angezeigte Wert auf @"Wert" vorgesetzt wird. Sie können das Programm jetzt testen. Es sei jedoch noch angemerkt, dass unsere Implementierung eigentlich nicht vollständig ist. Denn wir müssten in einer Subklasse von der Tabellenspalte ein weiteres Binding für den zusätzlichen Schlüsselpfad anbieten, welches ein entsprechendes Observing einrichtet. Abgesehen davon, dass Sie das genügend trainiert haben, ist es hier ausnahmsweise nicht erforderlich, weil Cocoa so nett ist, den Basiseintrag automatisch neu zu zeichnen, wenn ein Kind hinzugefügt wird. Glück gehabt …
HILFE Sie können das Projekt in diesem Zustand als Projekt »CustomOutlineView 5« von der Webseite herunterladen.
Control und Events Was das Aussehen betrifft, will man meist an die Cell. Geht es um erweiterte Funktionalität, bietet sich indessen der Weg an, ein Control oder ein sonstiges Standardview abzuleiten. Wir werden etwa die Löschtaste implementieren, mit der sich dann einzelne Einträge entfernen lassen. Aber auch hier noch einmal die Warnung vorab: Wenn sich dasselbe Ziel mittels Delegating erreichen lässt, sollten Sie die Finger von Subklassen lassen. Delegates first!
BEISPIEL Das Tableview lässt es etwa zu, dass Mausklicks abgearbeitet werden. Für unser Trachten existiert aber keine Methode im Delegate-Protokoll. Also greifen wir zur Ableitung. Bedenken Sie aber, dass auch die Möglichkeiten von Delegates wachsen. Daher kann zu einem späteren Zeitpunkt die Implementierung mittels Delegating die bessere werden. Womit wir auch bei der Kehrseite wären. Wenn wir jetzt gleich in einem Projekt Einträge mittels der Löschtaste entfernen, ist dies eigentlich eine typische Controlleraufgabe. Es wird ja das Model verändert. Wir sollten daher unseren Code für andere öffnen. So geben wir einem künftigen Nutzer unserer Klasse die Möglichkeit, in den Ablauf einzugreifen. Dies verhindert eine erneute Ableitung, wenn dieser wiederum unser Standardverhalten ändern will. Denkbar wäre hier etwa, dass bestimmte Einträge nicht löschbar sind.
420
Kapitel 4
Windows, Controls und Cells
Beginnen wir: Erzeugen Sie sich ein neues Projekt TableViewWithDelete als Cocoa-Application ohne Unterstützung für Dokumente, aber mit Unterstützung von Core Data. Fügen Sie dem Model einen Entitätstypen Person hinzu, dem Sie einfach zwei Eigenschaften firstName und lastName verpassen, beide vom Typ String. Erzeugen Sie bitte in Xcode die neue Klasse TableViewWithDelete mit File | New File…, wobei Sie als Vorlage Objective-C class und darunter als Subclass NSView wählen. Ändern Sie die Basisklasse in TableViewDelete.h auf NSTableView: @interface TableViewWithDelete : NSTableView { } @end
Leeren Sie bitte die Implementierung in TableViewWithDelete.m vollständig: @implementation TableViewWithDelete @end
Nach einem Doppelklick auf MainMenu.xib in Groups & Files ziehen Sie bitte aus der Library einen Array-Controller in das Hauptfenster MainMenu.xib. Setzen Sie im Attributes-Pane Mode auf Entity und Entity Name auf Person. Schalten Sie Prepares Content ein. Zuletzt binden Sie den Managed-Object-Context an das Application-Delegate. Fügen Sie dem Anwendungsfenster einen Tableview hinzu, den Sie der Library entnehmen. Im Identity-Pane setzen Sie jedoch die Basisklasse auf TableViewWithDelete: Das ist etwas frickelig. Nach dem Hereinziehen ist zunächst das umgebene Scrollview selektiert (Band I, S. 406 ff.). Daher müssen Sie sich erst einmal »in das Tableview klicken«, so dass dies auch in der Titelzeile des Inspectors erscheint. Dann erst ändern Sie bitte die Klasse. Die beiden Spalten des Tableviews bezeichnen Sie und binden sodann diese an den Array-Controller. Außerdem benötigen wir vorerst zwei Buttons zum Hinzufügen und Löschen von Einträgen. Auch in das Fenster. Diese verbinden Sie bitte mit den entsprechenden Methoden des Array-Controllers. Übersetzen und starten Sie die Anwendung. Können Sie entsprechende Einträge hinzufügen und entfernen? Dann weiter im Text …
HILFE Sie können das Projekt in diesem Zustand als Projekt »TableViewWith Delete 1« von der Webseite herunterladen.
421
SmartBooks
Objective-C und Cocoa – Band 2
Löschtaste empfangen Zunächst müssen wir überhaupt dafür sorgen, dass unser neues Tableview Tastendrücke empfängt. Dazu überschreiben wir die Methode -keyDown: @implementation TableViewWithDelete - (void)keyDown:(NSEvent*)event { NSString* text = [event charactersIgnoringModifiers]; if( [text length] == 1 ) { unichar key = [text characterAtIndex:0]; if( (key == NSDeleteCharacter) || (key == NSDeleteFunctionKey) ) { NSLog( @"Löschtaste" ); return; } } NSLog( @"Default" ); [super keyDown:event]; }
Eigentlich müssen wir hier nur das bekannte Wissen anwenden. Es wird abgefragt, ob ein einzelnes Zeichen anliegt, und falls dies der Fall ist, ob es sich um die Backspace-Taste (NSDeleteCharacter) oder die Löschtaste (NSDeleteFunctionKey) handelt. Sind die Bedingungen nicht erfüllt, so wird die Basisimplementierung aufgerufen. Dies ist notwendig, damit die Tastaturnavigation im Tableview noch funktioniert (und übrigens ohnehin eine gute Idee, wie bereits im zweiten Kapitel ausgeführt).
Löschung durchführen Bleibt die eigentliche Aufgabe, den entsprechenden Eintrag zu löschen. Und hier kommt uns, wie bereits eingangs erwähnt, das Model-View-Controller-Muster in die Quere. Es ist falsch bis unmöglich, einen Eintrag im Tableview zu löschen. Das Tableview zeigt diese ja nur an. Wir müssen also einen Controller darüber informieren, dass er löschen soll. Es liegt auf der Hand – und ist absolut richtig! –, dies über Delegating an einen Controller zu übertragen. Dazu würden wir uns ein Protokoll mit der entsprechen-
422
Kapitel 4
Windows, Controls und Cells
den Löschmethode definieren und bei Vorhandensein dieser Methode selbige aufrufen. Das kennen Sie allerdings schon zum Erbrechen. Sozusagen als billige Dreingabe wollen wir hier aber eine zweite Möglichkeit demonstrieren, aus einem – komplexen – View in die Controllerschicht zu kommen. Denn eigentlich ist ein Delegate als Controller überflüssig, weil wir ja schon einen Controller haben: Den Array-Controller. Und da sich dieser auf das Löschen versteht, können wir doch einfach den benutzen. Der Trick besteht in der Ihnen bereits aus Kapitel 3 bekannten Methode -infoForBinding:. Diese liefert uns ja das Ziel des Bindings. Und falls dort ein Array-Controller schlummern sollte, können wir gleich diesem die Löschnachricht schicken (und die albernen NSLog() herausnehmen): - (void)keyDown:(NSEvent*)event { NSString* text = [event charactersIgnoringModifiers]; if( [text length] == 1 ) { unichar key = [text characterAtIndex:0]; if( (key == NSDeleteCharacter) || (key == NSDeleteFunctionKey) ) { // Besteht ein Binding? NSDictionary* binding; binding = [self infoForBinding:NSContentBinding]; if( binding ) { // Zu einem Array-Controller? id obj = [binding objectForKey:NSObservedObjectKey]; if( [obj isKindOfClass:[NSArrayController class]] ) { [obj remove:self]; } } return; } } [super keyDown:event]; }
Wichtig ist hierbei, dass eigentlich ja nur die Tabellenspalten gebunden sind. Wie aber bereits erwähnt, führt die Bindung der ersten Spalte zur Bindung des Tableviews selbst. Das entsprechende Binding heißt, wie ersichtlich, content. 423
SmartBooks
Objective-C und Cocoa – Band 2
Im Prinzip macht dieser Code also nichts anderes, als nachzuschauen, ob das Tableview an einen Array-Controller gebunden ist. Ist dies der Fall, so wird dessen -remove: ausgeführt.
HILFE Sie können das Projekt in diesem Zustand als Projekt »TableViewWith Delete 2« von der Webseite herunterladen. Als Wissenskern nehmen Sie bitte hier mit, wie man durch Ableitung von Standardelementen deren Verhalten beeinflusst und vor allem, welche Wege es aus der Viewschicht in die darunter liegende Controllerschicht gibt. Merken!
Windows Kommen wir zuletzt zu einer weiteren häufig benötigten Anpassung des Standardverhaltens, an der wir gleich die Arbeit mit einem Fenster und ein paar Tricks zeigen können. Manchmal möchte man Fenster haben, die nicht rechteckig sind. Manchmal möchte man die Transparenz von Fenstern ändern. Und manchmal möchte man Löcher in Fenstern haben. Der Witz an der Sache: Es handelt sich dreimal um dasselbe Problem, welches entsprechend dreimal dieselbe Lösung hat: Transparenz, Transparenz, Transparenz. Denn zwar ist die Form eines Fensters mit einem Rechteck festgelegt. Wir können aber in diesem Rechteck transparent malen, so dass entsprechende Bereiche unsichtbar bleiben. Das Fensterrechteck legt also gleichermaßen nur die maximale Ausdehnung fest. Legen Sie wieder ein neues Projekt mit dem Namen Transparency an. Als Vorlage wählen Sie erneut Cocoa Application, wobei Sie den Support sowohl von Dokumenten wie auch von Core Data ausschalten.
Subklassen für Fenster und View Damit wir überhaupt etwas Eigenes machen können, müssen wir eine eigene Subklasse anlegen, die Sie TransparentWindow nennen. Dies machen Sie in gewohnter Manier, wobei Sie als Vorlage Objective-C class wählen und dann im Header die Basisklasse von Hand setzen:
424
Kapitel 4
Windows, Controls und Cells
@interface TransparentWindow : NSWindow { } @end
Außerdem werden wir ein eigenes View verwenden, welches unser Window »formt«. Also bitte noch eine Subklasse erzeugen, diesmal allerdings NSView als Basisklasse eintragen. Wir nennen es TransparentView. Wechseln Sie bitte zu MainMenu.xib in den Interface Builder und löschen Sie das Fenster window im Hauptfenster MainMenu.xib. Stattdessen ziehen Sie aus der Library Window and Drawer in das Hauptfenster. Verbinden Sie das window-Outlet des Application-Delegates mit dem neuen Window (Applikationsfenster). Im Identity-Pane setzen Sie die Klasse des neuen Applikationsfensters auf TransparentWindow. Außerdem setzen Sie im Attributes-Pane die Eigenschaft Textured. Ziehen Sie ein Custom-View in das Fenster und setzen Sie dessen Klasse auf TransparentView. Vergrößern Sie es so, dass es das Fenster bedeckt, wobei Sie den vorgeschlagenen Rand lassen. Ziehen Sie das Fenster nach unten etwas größer und platzieren Sie zwei Buttons: Show und Hide. Diese verbinden Sie mit den Actions open bzw. close des Drawers im Hauptfenster MainMenu.xib. Schließen Sie den Interface Builder wieder. Lassen wir zunächst etwas in dem View zeichnen, um unsere bisherige Arbeit zu überprüfen. TransparentView.m: - (void)drawRect:(NSRect)dirtyRect { [[NSColor redColor] setFill]; NSRectFill( [self bounds ] ); }
Wenn Sie die Applikation übersetzen und starten, sollte ein entsprechendes rotes Rechteck erscheinen. Mit den Buttons sollten Sie den Drawer öffnen und schließen können.
HILFE Sie können das Projekt in diesem Zustand als Projekt »TransparentWindow 1« von der Webseite herunterladen.
425
SmartBooks
Objective-C und Cocoa – Band 2
Transparenz Fangen wir zunächst damit an, dem View Transparenz beizubringen. Transparent ist ein View dann, wenn es teilweise mit transparenter Farbe bemalt ist. Dies ist nicht Schwarz oder Weiß, sondern wirklich transparente Farbe, Klarlack sozusagen. Wir müssen uns dabei allerdings nicht um Feinheiten der Implementierung kümmern, denn es existiert eine Systemfarbe clearColor. Experimentieren wir damit herum: - (void)drawRect:(NSRect)dirtyRect { NSRect bounds = [self bounds ]; [[NSColor clearColor] setFill]; NSRectFill( bounds ); bounds = NSInsetRect( bounds, bounds.size.width /10.0, bounds.size.height/10.0); [[NSColor redColor] setFill]; NSRectFill( bounds );
bounds = NSInsetRect( bounds, bounds.size.width /10.0, bounds.size.height/10.0); [[NSColor clearColor] setFill]; NSRectFill( bounds ); }
Wenn Sie das übersetzen und starten, werden Sie allerdings vermutlich enttäuscht sein. Denn anstelle von Transparenz erscheint Schwarz. Dies findet seine Ursache darin, dass unser Fenster als opaque, also »durchgängig bemalt« angenommen wird. Das machen wir aber gar nicht wirklich. Um das zu ändern, müssen wir den halbseidenen Charakter unseres Fensters bekanntgeben. Dazu ändern wir TransparentWindow.m: @implementation TransparentWindow - (id)initWithContentRect:(NSRect)frame styleMask:(NSUInteger)style backing:(NSBackingStoreType)backing defer:(BOOL)defer {
426
Kapitel 4
Windows, Controls und Cells
self = [super initWithContentRect:frame styleMask:style backing:backing defer:defer]; if( self ) { [self setOpaque:NO]; } return self; }
Bei -initWithContentRect:styleMask:backing:defer: handelt es sich um den Designated-Initializer von NSWindow. Diesen überschreiben wir also und setzen die Opacity (vollständige Deckung) auf NO. Wenn Sie jetzt die Applikation starten, sieht das schon deutlich besser aus: Es erscheint ein rechteckiger transparenter Ausschnitt, darin ein rotes Rechteck, darin wieder ein rechteckiger transparenter Ausschnitt. Öffnen und schließen Sie mal den Drawer … Jeck, nicht wahr? Auf zwei Dinge möchten wir Sie explizit aufmerksam machen:
•
Wir übermalen ein rotes Rechteck mit Klarlack. Vielleicht erwarten Sie, dass deshalb einfach das Rechteck innen rot bleibt. Aber nichts zu zeichnen ist etwas anderes als mit Klarlack zu zeichnen. Die Farbe »transparent« wird gesetzt, so dass das Rechteck an dieser Stelle tatsächlich wieder durchsichtig wird. Durch diesen Effekt entsteht das Loch.
•
Da das Fenster textured und das Subview selbst keine Mausevents abarbeitet, können Sie an der roten Fläche das Fenster bewegen. Da allerdings der Fenstermanager von OS X den Test, ob ein Fenster getroffen wurde, nicht einfach am Fensterrechteck, sondern an der tatsächlichen Darstellung vornimmt, führt ein Klick auf die transparente Fläche dazu, dass das dahinter liegende Fenster getroffen wird. Dieses springt dann nach vorne.
Um den zweiten Effekt, der ja möglicherweise unerwünscht ist, wollen wir uns noch kümmern. Glücklicherweise vergleicht der Fenstermanager, ob die entsprechende Stelle des Fensters wirklich gänzlich transparent ist. Daher ist es möglich, Treffer in das Innere des Rechteckes als Treffer auf unser Applikationsfenster zu werten. Wir malen das innerste Rechteck einfach mit ein ganz klein wenig Deckkraft: - (void)drawRect:(NSRect)dirtyRect { … [[NSColor colorWithCalibratedWhite:1.0 alpha:0.05] setFill]; NSRectFill( bounds ); }
427
SmartBooks
Objective-C und Cocoa – Band 2
Allerdings möchten wir vor dieser Möglichkeit ausdrücklich warnen, da die Farbauflösung des Displays, insbesondere des Alpha-Kanals (Deckkraft), eine Rolle spielt. Das Ganze ist also eher geschätzt. Und es ist auch nicht gerade nutzerfreundlich, wenn dem Anwender etwas vorgegaukelt wird. Sie sollten indessen einmal höhere Werte für Alpha probieren, um so halbtransparente Effekte zu sehen. Übrigens lässt sich auch noch mittels -setAlphaValue: für das gesamte Fenster eine Transparenz einstellen. Sie können damit mal im Designated-Initializer des Fensters testweise herumspielen: - (id)initWithContentRect:(NSRect)frame styleMask:(NSUInteger)style backing:(NSBackingStoreType)backing defer:(BOOL)defer { … if( self ) { … [self setOpaque:NO]; [self setAlphaValue:0.75]; } return self; }
Wie Sie sehen, betrifft diese Transparenz das gesamte Fenster einschließlich der Systembereiche Titelzeile usw. Entfernen Sie das wieder.
HILFE Sie können das Projekt in diesem Zustand als Projekt »TransparentWindow 2« von der Webseite herunterladen.
Konfiguration Allerdings hat unser Fenster noch keine eigene Form, da die Systembestandteile wie Titelzeile noch vorhanden sind. Wir können auf die Eigenschaften eines Fensters sehr viel mehr Einfluss nehmen, wenn wir es nicht im Xib erzeugen, sondern aus dem Code heraus. Löschen Sie in MainMenu.xib das Fenster mitsamt dem Drawer und dessen Content-View. 428
Kapitel 4
Windows, Controls und Cells
Aufgepasst: Da wir das Fenster nicht mehr aus einem Nib-File laden, müssen wir es selbst im Speicher halten. Daher ist im Header des Application-Delegates die Property anzupassen. Das Outlet entfernen wir gleich auch: @interface TransparencyAppDelegate : NSObject { NSWindow *window; } @property( retain ) NSWindow *window; @end
Wir erzeugen das Fenster jetzt im Application-Delegate: #import "TransparencyAppDelegate.h" #import "TransparentView.h" #import "TransparentWindow.h" … - (void)applicationDidFinishLaunching:(NSNotification *)notif { NSRect frame = [[NSScreen mainScreen] frame]; if( frame.size.width > frame.size.height ) { frame.size.width = frame.size.height; } else { frame.size.height = frame.size.width; } frame = NSInsetRect( frame, frame.size.width/4, frame.size.height/4 ); NSUInteger style = NSTexturedBackgroundWindowMask; NSUInteger backing = NSBackingStoreBuffered; BOOL deferred = NO; TransparentWindow* newWindow; newWindow = [[[TransparentWindow alloc] initWithContentRect:frame styleMask:style backing:backing defer:deferred] autorelease]; [newWindow setReleasedWhenClosed:NO];
429
SmartBooks
Objective-C und Cocoa – Band 2
TransparentView* view; view = [[[TransparentView alloc] initWithFrame:NSZeroRect] autorelease]; [newWindow setContentView:view]; self.window = newWindow; [newWindow makeKeyAndOrderFront:self]; }
Was passiert im Einzelnen? Nachdem das Rechteck ermittelt wird, erzeugen wir das Fenster mit einigen Optionen: Style-Mask Die mit Abstand wichtigste Einstellungsmöglichkeit ist die styleMask, die Sie bereits oben aus dem Designated-Initializer kennen. Dabei existieren verschiedene Werte:
•
NSBorderlessWindowMask sorgt für ein Fenster ohne Ränder, Systemelemente wie Titelzeile und sonstige Eigenschaften.
•
NSTitledWindowMask, NSClosableWindowMask, NSMiniaturizableWindow Mask, NSResizableWindowMask bezeichnet die verschiedenen Fensterelemente, welche optional eingeblendet werden können.
•
NSTexturedBackgroundWindowMask wird von uns verwendet, um das Fenster über den Fensterhintergrund verschiebbar zu machen.
•
NSUnifiedTitleAndToolbarWindowMask bezeichnet ein Fenster, bei dem die Titelzeile in die Toolbar graphisch übergeht.
•
NSUnscaledWindowMask - Wie Sie bereits wissen, können das »virtuelle« Koordinatensystem und das Pixelkoordinatensystem divergieren. Wird diese Option verwendet, so erfolgt keine entsprechende Skalierung.
Diese Optionen können mittels des Bit-Oder-Operators | verknüpft werden. Backing Der nächste Parameter bestimmt, ob unmittelbar auf dem Bildschirm oder zunächst in einen Puffer gezeichnet werden soll. Hierbei handelt es sich um eine übernommene Einstellung für eine frühere Hardware, die nicht so leistungsfähig war. Aktuell ist nur der von uns angegebene Wert zum Puffern sinnvoll. Alles andere führt zu einem Performanceverlust oder bleibt ohne Wirkung.
430
Kapitel 4
Windows, Controls und Cells
Defer Mit dieser Option wird bestimmt, ob unmittelbar im Fenstermanager von OS X ein entsprechendes Fenster erzeugt werden soll. Ist diese Option YES, so wird gewartet, bis das Fenster tatsächlich auf dem Bildschirm erscheint. Dies spart Speicher, was wichtig werden kann, wenn viele geschlossene Fenster existieren. Als Cocoa-Junkie sollten Sie aber ohnehin Fenster in eigene Nibs packen (Band I, S. 471 f.). Released when closed Hiermit wird angegeben, ob sich das Fenster selbst ein release schicken soll, wenn es geschlossen wird. Derlei Tricks aus der Steinzeit der Speicherverwaltung wenden wir nicht an. Aber bei einem Applikationsfenster ist dies ohnehin sinnlos. Shadow Die Eigenschaft Shadow muss wohl nicht weiter erläutert werden. Contentview Hiernach erzeugen wir eine Instanz unserer Viewklasse und setzen diese als Contentview. Das Contentview ist das oberste View im Applikationsbereich (Band I, S. 338 f.) Wir setzen uns also gleich an die Wurzel. Weitere Eigenschaften Wie Sie der Dokumentation zu NSWindow entnehmen können, existieren eine Vielzahl von weiteren Konfigurationsmöglichkeiten. Sie sollten sich die Liste einmal durchsehen. Vor einem Missverständnis wollen wir jedoch explizit warnen: Child-Windows sind nicht etwa »Fenster in Fenstern«, wie Sie das vielleicht von Windows kennen. Vielmehr handelt es sich um weitere, graphisch losgelöste Fenster, die lediglich die Bewegungen des Elternfensters automatisch mitmachen. Auf diese Weise sind Drawer implementiert. Starten Sie die Anwendung. Sie werden unseren View nun ganz ohne »sichtbares« Fenster bewundern können. Ach, hatten Sie sich eigentlich mal gefragt, wieso es im Interface Builder möglich ist, Connections zwischen zwei Fenstern zu ziehen? Der Trick ist einfach: Sobald eine Connection gezogen wird, wird über den gesamten Bildschirm ein Fenster gelegt, welches außer Transparenz nur die Verbindungslinie enthält. Auch Menüs und dergleichen sind in Wahrheit Fenster. Vielleicht bemerken Sie noch, dass sich bei unserer Applikation das Rechteck nicht bis an den oberen Rand ziehen lässt. Denken Sie aber daran, dass unser View außen aus Transparenz besteht. Das Fenster mit dem View ist tatsächlich bereits an die Menüleiste angestoßen, zeichnet unterhalb bloß nichts außer Transparenz.
431
SmartBooks
Objective-C und Cocoa – Band 2
Field-Editor Eigentlich keine große Sache, sondern etwas, was man einfach kennen muss, ist der ebenfalls aus Performancegründen vorhandene Field-Editor. Er ist eine Instanz der Klasse NSText, wie Sie vielleicht oben schon an den Editiermethoden gesehen haben. Das Wesentliche ist, dass er standardmäßig nur ein Mal pro Fenster existiert und für alle einfachen Texteingabefelder die Aufgabe der Bearbeitung durch den Nutzer übernimmt. Dies wird technisch erledigt, indem der Field-Editor in der Responder-Chain vor das entsprechende Textfeld gesetzt wird. Damit landen die über die ResponderChain verteilten Nachrichten zunächst bei diesem, der sie umfassend verarbeitet und daher nicht mehr weiterleitet. NSWindow fieldEditor contentView … NSView nextResponder subviews … NSTextField subviews nextResponder … NSTextField subviews nextResponder … NSTextView nextResponder … Sobald editiert wird, setzt sich der Field-Editor des Fensters an den Anfang der Responderchain.
432
Kapitel 4
Windows, Controls und Cells
Dies führt dazu, dass es für die Eingabe keine Änderung bedeutet, wenn Sie eine Subklasse von NSTextField an ein Textfeld zuweisen. Denn im Moment der Eingabe wird diesem ohnehin durch den Field-Editor des Fensters über den Mund gefahren. Legen Sie sich ein neues Projekt aus der Vorlage Cocoa Application ohne Unterstützung für Core Data und Dokumente an und nennen Sie es CustomFieldEditor. Erzeugen Sie sich aus der Vorlage Objective-C class|NSView eine Subklasse von NSTextField: @interface CustomTextField : NSTextField { } @end
In der Implementierung passen wir das Verhalten an: @implementation CustomTextField - (void)drawRect:(NSRect)dirtyRect { [super drawRect:dirtyRect]; } - (void)keyDown:(NSEvent*)event { NSBeep(); [super keyDown:event]; } @end
Sie sehen schon, dass da nicht wirklich etwas Spannendes passiert. Es geht hier alleine darum, die Aufrufe anzuzeigen, was dadurch geschieht, dass NSBeep() einen Ton ausgibt. Im Interface Builder ziehen Sie einfach zwei Textfelder in das Programmfenster und stellen Sie bei einem der Textfelder im Identity-Pane des Inspectors die neue Subklasse CustomTextField als Klasse ein. Wenn Sie das Programm testen, werden Sie bemerken, dass Sie während des Editierens nichts hören. Das liegt daran, dass der Field-Editor an die Stelle des Textfeldes getreten ist.
Anpassung Den Field-Editor eines Fensters erhalten Sie mit -fieldEditor:forObject:, wobei Sie einfach nil für das Objekt einsetzen können, wenn Sie den Standardeditor möchten. 433
SmartBooks
Objective-C und Cocoa – Band 2
Es existieren verschiedene Möglichkeiten, den Field-Editor in seinem Verhalten anzupassen: -setFieldEditor: Schickt man an den (Standard-)Field-Editor die Nachricht setFieldEditor: mit dem Parameter NO, so wird die Betätigung der Tasten [r], [t] und [s]+[t] nicht vom Field-Editor abgefangen, sondern landen bei dem Textfeld. Dadurch können diese vom Benutzer eingegeben werden. Abgesehen von dieser sehr begrenzten Möglichkeit, gilt dies dann gleich für alle Textfelder. Na ja, … - (void)applicationDidFinishLaunching:(NSNotification*)notif { [[window fieldEditor:YES forObject:nil] setFieldEditor:NO]; }
HILFE Sie können das Projekt in diesem Zustand als Projekt »CustomFieldEditor« von der Webseite herunterladen. Entfernen Sie bitte diesen Code wieder, wenn Sie dies einmal ausprobiert haben. Beeps hören Sie freilich so immer noch nicht. Delegating Geschickter als diese Arme-Leute-Lösung ist es, das Delegating des Field-Editors zu nutzen. Denn der Field-Editor ist eine Instanz von NSTextView, die über umfassende Delegatemethoden verfügt.
HILFE Die Klasse NSTextView modelliert ein umfassendes Textfeld, welches auch Textattribute usw. versteht. Wir besprechen dies im Kapitel über das Textsystem. Und noch besser: Der Field-Editor macht das usprüngliche Textfeld zu seinem Delegate. Es reicht also aus, eine der Delegatemethoden in der Subklasse CustomTextField einzubauen: @implementation CustomTextField - (BOOL) textView:(NSTextView*)textView shouldChangeTextInRange:(NSRange)affectedCharRange replacementString:(NSString*)replacementString
434
Kapitel 4
Windows, Controls und Cells
{ NSBeep(); return YES; }
Jetzt hören Sie auch wieder den Beep, und zwar nur bei dem im Focus stehenden Textfeld. Ableitung Wenn gar nichts mehr geht, dann können Sie den Field-Editor auch ableiten, wobei Cocoa als Basisklasse für den Field-Editor NSTextView nimmt. Schön ist das freilich nicht. Um den ensprechenden Field-Editor für ein bestimmtes Textfeld zu installieren, dient die Delegatemethode -windowWillReturnFieldEditor:toObject:. Hier können Sie also eigene Instanzen Ihrer Subklasse anbieten.
Zusammenfassung Sie haben die Standardmechanismen wichtiger Klassen der Viewschicht kennengelernt. Natürlich ist dies ein weites Feld, auch ganz ohne Literaturkritik. Aber Sie sollten grundlegende Mechanismen gesehen haben:
• • • • • •
Die Zusammenarbeit von Controls und Cells Ableitung von Controls Ableitung von Cells Datenaustausch zwischen Controls und Cells und wieder zurück Grundlagen der Fensteranpassung Anpassung des Editierverhaltens mit einem Field-Editor.
435
SmartBooks
Objective-C und Cocoa – Band 2
436
Core Image und Core Animation
5
Die graphische Ausgabe mit Bezier-Pfaden kennen Sie schon. Auch haben wir schon auf »klassische« Weise Images ausgegeben und erzeugt. Durch stetige Integration von Standardtechnologien wachsen jedoch die Möglichkeiten, die OS X und letztlich Cocoa oder verwandte Frameworks anbieten. Core Image und Core Animation gehören dazu. Sie sind nicht notwendig, verschönern aber eine Anwendung ungemein.
SmartBooks
Objective-C und Cocoa – Band 2
Core Image und Core Animation Für Mac-Applikationen ist es nicht unwesentlich, dass diese schön aussehen. Sie werden es selbst kennen: Sie starten eine neue Anwendung und haben sofort ein »Oh« auf den Lippen. Für die schnelle und einfache Implementierung existieren Core Image und Core Animation.
Core Image Core Image ist ein Framework, welches Bilder und Videos in Echtzeit manipulieren kann. Hierbei bedient sich Core Image höchst komplizierter Methoden aus der Mathematik und Informatik. Diese Kompliziertheit wird von Core Image allerdings gut versteckt. Apple hat es mit Core Image geschafft, Entwicklern ein einfaches und pragmatisches Framework anzubieten. Manipulationen werden durch einen oder mehrere Filter beschrieben. Apple hat Core Image mit ungefähr einhundert Filtern ausgestattet, die direkt benutzt werden können. Da Core Image eine erweiterbare Architektur ist, kann jeder neue Filter entwickeln und diese mit Core Image nutzen. Bereits mit Standardfiltern lassen sich sehr einfache, aber auch sehr komplexe Manipulationen beschreiben. Die folgenden Abbildungen sollen Ihnen einen kleinen Eindruck der Möglichkeiten vermitteln, die mit Standardfiltern möglich sind.
Einige Core Image-Effekte
438
Kapitel 5
Core Image und Core Animation
Die verfügbaren Filter werden von Core Image kategorisiert, wobei ein Filter in mehreren Kategorien gelistet sein kann. Es existieren die folgenden Kategorien: Name CICategoryBlur CICategoryColorAdjustment
Beschreibung der Filter der Kategorie Filter zum Weichzeichnen Filter zur Manipulation der Sättigung, des Kontra stes, der Helligkeit, des Farbtons, der Belichtung, ...
CICategoryColorEffect CICategoryComposite Operation
Filter zur Erzeugung monochromer Bilder, zum Invertieren der Farben eines Bildes, zur Erzeugung von Speziaeffekten, ... Filter zum Überlagern beziehungsweise Zusammenstellen von Bildern
CICategoryDistortion Effect
Filter zur Verzerrung (Quetschen, Erzeugung von Wirbeln und Beulen, ...) von Bildern
CICategoryGenerator
Filter zur Erzeugung von »Schachbrettern«, Streifen, zufälligen Bildern (verrauschten Bildern), Sonnenstrahlen, Sternen, ...
CICategoryGeometry Adjustment
Filter zur Anwendung affiner Transformationen, zum Zurechtschneiden von Bildern, zur Anwendung einer »Lanczos-Skalierungs-Transformation« (zur performanten Erzeugung von Vorschaubildern (Thumbnails)) und für perspektivische Transformationen
CICategoryGradient Filter zur Erzeugung von Farbverläufen CICategoryHalftoneEffect Filter zur Erzeugung von Rastern CICategoryReduction Filter zur Erzeugung von (Single-Pixel)-Bildern, die gewisse Eigenschaften beschreiben. Zum Beispiel erzeugt der CIAreaAverage-Filter ein Einquadratpixel großes Bild, welches die durchschnittliche Farbe eines bestimmten Ausschnittes eines Bildes repräsentiert. CICategorySharpen CICategoryStylize
CICategoryTileEffect CICategoryTransition
Filter zum Scharfzeichnen Filter zum »Stilisieren« (Comiceffekt, Betonen von Kanten in einem Bild, Vergrößern der Pixel eines Bildes (engl.: Pixelation), Erzeugen eines Scheinwerferlichtes, ...) Filter zum »kacheln« von Bildern Filter zum effektvollen Übergang eines Bildes in ein anderes. Siehe: CIPageCurlTransition. 439
SmartBooks
Objective-C und Cocoa – Band 2
Apple beschreibt alle Filter in einem Dokument mit dem Titel Core Image Filter Reference. Der Umgang mit dieser Referenz ist nicht ganz so einfach. Seien Sie aber nicht besorgt. Gleich im ersten Beispiel werden Sie den Umgang mit der Filterreferenz erlernen.
Die Anwendung Core Image Fun House Bevor wir zum ersten Beispiel schreiten, möchten wir Ihnen die Anwendung Core Image Fun House vorstellen. Diese Anwendung wird automatisch mit den Entwicklerwerkzeugen installiert. Sie finden Core Image Fun House am besten, indem Sie mit Spotlight danach suchen. Nach dem Starten erscheint ein Dialog, der Sie auffordert, ein Bild auszuwählen. Wählen Sie ein Bild aus und klicken Sie auf Open. Das ausgewählte Bild wird dann in einem Fenster angezeigt. Zusätzlich zu dem Fenster, welches das Bild anzeigt, wird noch ein Panel, der Effect Stack, geöffnet. Mit einem Klick auf den +-Button können Sie auf das Bild einen beliebigen Core Image-Filter anwenden.
Mit Core Image Fun House können vorhandene Filter ausprobiert werden.
Wählen Sie, wie es die Abbildung zeigt, den Filter Color Controls aus und klicken Sie auf Apply. Im Effect Stack werden für den Color-Controls-Filter drei Slider erzeugt, mit denen Sie die Eigenschaften des Filters ändern können. Beim Ändern der Werte der Slider wird das Bild in Echtzeit, entsprechend den Werten, manipuliert.
440
Kapitel 5
Core Image und Core Animation
Der Filter »CIColorControls«, angewandt auf ein Bild
Diese kleine Demonstration soll Ihnen verdeutlichen, wie leistungsstark Core Image ist. Obwohl das ausgewählte Bild recht groß ist, werden Änderungen an den Werten sofort sichtbar. Sie können jetzt noch weitere Filter hinzufügen und kombinieren. Experimentieren Sie ruhig ein wenig mit Core Image Fun House. Es ist ein guter Weg, um die verschiedenen Filter kennenzulernen. Die Anwendung ist natürlich mit Core Image realisiert worden. Dies bedeutet, dass Sie jeden Filter und jede Kombination mehrerer Filter, die Sie dort erzeugen, auch in Ihrer eigenen Anwendung nutzen können.
Das Handwerkszeug: die Core Image-Klassen Core Image Fun House zeigte Ihnen, dass Core Image viele Filter mitbringt und diese sich beliebig kombinieren lassen. Fast alle Filter verfügen über gewisse Parameter, deren Werte Sie in Core Image Fun House ändern konnten. Diese Funktionalität wird von Core Image in einigen Klassen abgebildet.
441
SmartBooks
X Y Z ...
Objective-C und Cocoa – Band 2
CIVector
CIColor red green blue alpha ... CIImage definition extent ...
beschreiben
zeichnet
CIFilter attributes inputKeys outputKeys ...
beschreiben
CIFilterGenerator exportedKeys classAttributes filter ...
CIContext -drawImage:atPoint:fromRect: ...
Die wichtigsten Core Image-Klassen
Filter werden von Core Image durch die Klasse CIFilter beschrieben. Ein Filter verfügt in der Regel über ein oder mehrere Eingabe- und Ausgabeparameter. Die Eingabeparameter sind Informationen, die ein Filter zur Erzeugung eines Resultates benötigt. Fast alle Filter benötigen als Eingabeparameter unter anderem ein Bild (das sogenannte Input-Image) und erzeugen als Resultat ebenfalls ein Bild (das sogenannte Output-Image). Eingabeparameter sind in der Regel durch Instanzen der Klassen CIVector, CIColor und CIImage beschrieben. Nehmen wir als Beispiel den Filter CISpotLight, der ein Scheinwerferlicht erzeugt. Ein Scheinwerferlicht wird unter anderem durch die Angabe seiner Position und Richtung, in die er leuchtet, bestimmt. Diese Eigenschaft ist als Klasse CIVector modelliert. Jeder CIVector bestimmt dabei einen Vektor. Weiterhin ist es möglich, die Farbe des Scheinwerferlichtes zu bestimmen. Farben sind im Zusammenhang mit Core Image immer mit Instanzen der Klasse CIColor festgelegt. Core Image muss natürlich auch wissen, welches Bild das Scheinwerferlicht zu beleuchten hat. Bilder werden durch Instanzen der Klasse CIImage repräsentiert. Durch Bilder, Vektoren und Farben können allerdings nicht alle Filter beschrieben werden. Beispielsweise wird die Helligkeit des Scheinwerferlichtes durch Instanzen der Klasse NSNumber festgelegt. Der Filter CIAffineTransform benötigt als Eingabeparameter eine Instanz der Klasse NSAffineTransform, die die Transformation spezifiziert, die der Filter auf sein Eingabebild anwenden soll. Mehrere Filter können wiederum benutzt werden, um einen CIFilterGenerator zu erzeugen. Am Ende der Kette befindet sich der Core Image Kontext, der durch eine Instanz der Klasse CIContext repräsentiert wird. Ein CIContext kann zum Erzeugen von Core Graphics-Bildern und Core Graphics-Layern sowie zum Zeichnen von CIImages genutzt werden.
442
Kapitel 5
Core Image und Core Animation
Core Image-Filter nutzen Im Rahmen dieses Abschnitts entwickeln wir eine Anwendung, die in einem Imageview ein Bild anzeigt. Mit drei Slidern kann der Benutzer den Konstrast, die Helligkeit und die Sättigung ändern. Das Bild repräsentiert immer die aktuellen Werte der Slider. Die Werte der Slider werden sofort auf das Bild angewendet. Der Benutzer spart sich also den Klick auf einen OK-Button und sieht das Ergebnis ohne Verzögerung. Die Manipulation des Bildes soll natürlich mit Hilfe von Core Image realisiert werden. Erzeugen Sie ein neues Projekt namens ColorControls vom Typ Cocoa Application. Da das Core Image-Framework Teil des Quartz Core-Frameworks ist, fügen Sie bitte das Quartz Core-Framework zum Projekt hinzu. Achten Sie darauf, dass das Framework in der Gruppe Frameworks | Linked Frameworks landet. Wie üblich wird ein Objekt benötigt, welches die Benutzereingaben auswertet und entsprechend reagiert. Dafür bietet sich, die von Xcode bereits angelegte Klasse ColorControlsAppDelegate an. Eine Instanz dieser Klasse soll bei Änderungen eines Wertes eines Sliders (für Kontrast, Helligkeit und Sättigung) das Bild im Imageview entsprechend des neuen Wertes ändern. Um auf Änderungen der Werte der drei Slider reagieren zu können, benötigt die Klasse drei Actionmethoden. Die Actionmethode -takeSaturationFromSender: wird vom Slider für die Sättigung aufgerufen, -takeContrastFromSender: für den Kontrast und -takeBrightnessFromSender: für die Helligkeit. Da unser ColorControlsAppDelegate das Bild eines Imageviews ändern können muss, benötigt es ein Outlet auf ein Imageview. Der Filter CIColorControls, den Sie einmal mit der Anwendung Core Image Funhouse testen können, benötigt als Eingabeparameter ein Bild und je einen Wert für die Sättigung, den Kontrast und die Helligkeit. CIColorControls-Filter inputImage inputSaturation = 2.0 inputBrightness = 0.0 inputContrast = 4.0 outputImage
Eingabe- und Ausgabeparameter des Filters »CIColorControls«
Das ColorControlsAppDelegate manipuliert das anzuzeigende Bild mit Hilfe eines CIColorControls-Filters. Damit ergibt sich für die Klasse ColorControlsAppDelegate das folgende Interface. 443
SmartBooks
Objective-C und Cocoa – Band 2
#import #import @interface ColorControlsAppDelegate : NSObject { CIFilter* filter; IBOutlet NSImageView* outputImageView; } #pragma mark Actions - (IBAction)takeSaturationFromSender:(id)sender; - (IBAction)takeBrightnessFromSender:(id)sender; - (IBAction)takeContrastFromSender:(id)sender; @end
Öffnen Sie die Datei MainMenu.xib und passen Sie im Interface Builder das Interface so an, dass es der folgenden Abbildung entspricht.
NSImageView
Verbunden mit -takeContrastFromSender: Minimum Value: 0.3 Maximum Value: 4.0 Current Value: 1.0
Verbunden mit -takeBrightnessFromSender: Minimum Value: -1.0 Maximum Value: 1.0 Current Value: 0.0
Verbunden mit -takeSaturationFromSender: Minimum Value: 0.0 Maximum Value: 2.0 Current Value: 1.0
Das Interface der Beispielanwendung
Stellen Sie sicher, dass Sie im Attribute-Pane des Inspectors die Checkbox der Eigenschaft continuous eines jeden Sliders mit einem Haken versehen. Dies hat zur 444
Kapitel 5
Core Image und Core Animation
Folge, dass die Slider bei Änderung ihres Wertes kontinuierlich die entsprechende Actionmethode aufrufen. Lassen Sie uns nun einen Blick auf die Implementierung der Klasse ColorControlsAppDelegate werfen. Zunächst werden die notwendigen Headerdateien eingebunden. #import "ColorControlsAppDelegate.h"
Anschließend werden in einer privaten Kategorie die beiden Eigenschaften filter und originalImage definiert. Die Eigenschaft filter ist vom Typ CIFilter und enthält den aktuell auf das angezeigte Bild angewandten Filter. Die Eigenschaft originalImage ist vom Typ NSImage und liefert das originale Bild, auf das der Filter angewandt werden soll. Ebenfalls in der privaten Kategorie wird die Methode -updateOutputImageView deklariert. Diese Methode wird später nach jeder Änderung des Wertes eines Sliders aufgerufen, um den Filter auf das originale Bild anzuwenden und um das Output-Image im Imageview anzuzeigen. @interface ColorControlsAppDelegate() #pragma mark Properties @property( retain ) CIFilter* filter; @property( readonly ) NSImage* originalImage; - (void)updateOutputImageView; @end
Im nächsten Schritt werden die Eigenschaften filter und outputImageView synthetisiert und die getter-Methode der Eigenschaft originalImage implementiert. @implementation ColorControlsAppDelegate #pragma mark Properties @synthesize filter; - (NSImage *)originalImage { return [NSImage imageNamed:@"me"]; } …
Natürlich müssen noch die Actionmethoden implementiert werden. … #pragma mark Actions - (IBAction)takeSaturationFromSender:(id)sender
445
SmartBooks
Objective-C und Cocoa – Band 2
{ } - (IBAction)takeBrightnessFromSender:(id)sender { } - (IBAction)takeContrastFromSender:(id)sender { } …
Wundern Sie sich nicht. Hier passiert wirklich noch nichts. Das kommt alles später. Wir konzentrieren uns zunächst darauf, dass das Gerüst steht. Nun fehlt noch die Implementierung von -updateOutputImageView und -dealloc. … - (void)updateOutputImageView { } #pragma mark Instantiation - (void)dealloc { self.filter = nil; [super dealloc]; } @end
Der Methode -updateOutputImageView werden wir ebenfalls erst später Leben einhauchen. Vergewissern Sie sich jetzt, dass Ihr Quellcode beim Kompilieren keine Fehler und auch keine Warnungen erzeugt und dass sich im Resources-Ordner in Xcode ein Bild namens »me« befindet.
HILFE Sie können das Projekt in diesem Zustand als »Color Controls 1« von der Webseite herunterladen. Das Grundgerüst der Anwendung steht. Im nächsten Schritt werden Sie das Grundgerüst erweitern. Zum einen muss die -init-Methode überschrieben werden. In der Methode -init soll der CIColorControls-Filter erzeugt und konfiguriert werden. 446
Kapitel 5
Core Image und Core Animation
Zum anderen wird in der Methode -awakeFromNib die originale Version (originalImage) im outputImageView angezeigt. Zusätzlich dazu wird in den Actionmethoden -takeSaturationFromSender:, -takeBrightnessFromSender: und -takeContrastFromSender: der Filter angepasst und die Methode -updateOutputImageView aufgerufen. Beginnen wir mit der -init-Methode. #pragma mark Instantiation - (id)init { self = [super init]; if( self ) { CIFilter* colorControlsFilter = [CIFilter filterWithName:@"CIColorControls"]; self.filter = colorControlsFilter; [self.filter setDefaults]; NSData* data = [self.originalImage TIFFRepresentation]; CIImage* image = [CIImage imageWithData:data]; [self.filter setValue:image forKey:kCIInputImageKey]; } return self; }
Die Instanz von CIFilter wird mit der Klassenmethode +filterWithName: erzeugt. Als Argument erwartet diese Methode den Namen eines Filters, hier also CIColorControls. Jeder Filter verfügt über einen eindeutigen Namen. Die Namen der verfügbaren Filter finden Sie im Dokument mit dem Titel Core Image Filter Reference.
GRUNDLAGEN Die Tatsache, dass ein Filter durch Angabe eines Namens erzeugt werden kann und dass es eine einzige Klasse (CIFilter) für alle möglichen Filter gibt, ist typisch für Cocoa. Ihnen ist vielleicht schon aufgefallen, dass es in Cocoa viele sehr umfangreiche Klassen gibt. Einerseits widerspricht dies dem Prinzip der Objektorientierung, Interfaces von Klassen möglichst minimal zu halten. Andererseits ist es ein sehr pragmatischer Ansatz, der sich über Jahre bewährt hat. CIFilter ist ebenfalls eine Klasse, die sehr viel kann. Es gibt ein Interface für alle Filter und nicht für jeden Filter ein eigenes. Jeder Filter ist implementiert als eine Subklasse von CIFilter. Um einen Filter zu konfigurieren, ist es aber nicht notwendig, dessen Klasse zu kennen. Dank KVC müssen Sie keine Accessoren nutzen, um die Eingabeparameter zu setzen.
447
SmartBooks
Objective-C und Cocoa – Band 2
Im nächsten Schritt wird die Methode -setDefaults von CIFilter aufgerufen. Dieser Aufruf setzt alle Werte der Eingabeparameter auf Standardwerte. Der Filter ColorControlsController ist in der Lage, den Konstrast, die Helligkeit und die Sättigung eines Bildes zu ändern. Das Bild, welches modifiziert werden soll, muss dem Filter mitgeteilt werden. Bilder werden im Zusammenhang mit Core Image durch die Klasse CIImage beschrieben. Instanzen von CIImage können unter anderem durch die Klassenmethode +imageWithData: erzeugt werden. Genau diese Methode nutzen wir auch im Beispiel. Auf das originale Bild, welches mit -originalImage ermittelt wird, wenden wir die Methode -TIFFRepresentation an, welche die Daten des Bildes als Instanz der Klasse NSData liefert. Mit diesen Daten erzeugen wir dann eine Instanz von CIImage. Merken Sie sich bitte dieses Verfahren, um aus einer Instanz von NSImage eine Instanz von CIImage herszustellen. Jede Instanz von CIIFilter verfügt über Eingabeparameter und über Ausgabeparameter. Die Werte der Eingabeparameter und Ausgabeparameter können durch Key-Value-Coding (KVC) gesetzt und gelesen werden. Der Eingabeparameter namens inputImage legt das Bild fest, welches der Filter manipulieren soll. Diesen Parameter setzen wir mit -setValue:forKey: auf die erzeugte Instanz von CIImage. Die Namen und möglichen Werte der Eingabeparameter und Ausgabeparameter finden Sie ebenfalls im Dokument mit dem Titel Core Image Filter Reference. In der Methode -awakeFromNib veranlassen wir, dass das originale Bild im outputImageView angezeigt wird. #pragma mark Instantiation - (void)awakeFromNib { outputImageView.image = self.originalImage; }
In den Actionmethoden -takeSaturationFromSender:, -takeBrightnessFromSender: und -takeContrastFromSender: geschieht nahezu das Gleiche. Es wird der Wert des Sliders ermittelt, daraus eine Instanz von NSNumber erzeugt, der Wert des entsprechenden Eingabeparameters des Filters auf die NSNumber-Instanz gesetzt und die Methode -updateOutputImageView aufgerufen. #pragma mark Actions - (IBAction)takeSaturationFromSender:(id)sender { float saturation = [sender floatValue]; NSNumber* newValue = [NSNumber numberWithFloat:saturation]; [self.filter setValue:newValue forKey:kCIInputSaturationKey]; [self updateOutputImageView]; }
448
Kapitel 5
Core Image und Core Animation
- (IBAction)takeBrightnessFromSender:(id)sender { float brightness = [sender floatValue]; NSNumber* newValue = [NSNumber numberWithFloat:brightness]; [self.filter setValue:newValue forKey:kCIInputBrightnessKey]; [self updateOutputImageView]; } - (IBAction)takeContrastFromSender:(id)sender { float contrast = [sender floatValue]; NSNumber* newValue = [NSNumber numberWithFloat:contrast]; [self.filter setValue:newValue forKey:kCIInputContrastKey]; [self updateOutputImageView]; }
Wie Sie sehen, hat jeder Eingabeparameter einen eigenen Namen. Der Eingabeparameter für die Sättigung heißt Input-Saturation, der für die Helligkeit InputBrightness und der für den Kontrast Input-Contrast. Stellen Sie sicher, dass Ihr Projekt ohne Fehler und Warnungen kompiliert. Jetzt fehlt noch die Implementierung der Methode -updateOutputImageView. Das kommt im nächsten Schritt.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Color Controls 2« von der Webseite herunterladen. Nun schreiten wir zur Implementierung der Methode -updateOutputImageView, die das durch den Filter manipulierte Bild im outputImageView anzeigt. - (void)updateOutputImageView { CIImage *outputImage = [self.filter valueForKey:kCIOutputImageKey]; CGFloat width = CGRectGetWidth( [outputImage extent] ); CGFloat height = CGRectGetHeight( [outputImage extent] ); NSSize size = NSMakeSize( width, height ); NSImage *newImage = [[[NSImage alloc] initWithSize:size] autorelease];
449
SmartBooks
Objective-C und Cocoa – Band 2
id rep = [NSCIImageRep imageRepWithCIImage:outputImage]; [newImage addRepresentation:rep]; outputImageView.image = newImage; }
Zunächst wird das vom Filter erzeugte Bild ermittelt. Das erzeugte Bild verbirgt sich hinter dem Ausgabeparameter namens outputImage. Das Ausgabebild ist eine Instanz der Klasse CIImage. Ein CIImage kann nicht direkt in einem Imageview angezeigt werden. Es muss zunächst in ein Image umgewandelt werden. Die Methode -extent liefert Informationen über die Ausmaße des Bildes. Anhand dieser Informationen wird mit -initWithSize: ein Image erstellt. Anschließend wird mit der Klassenmethode +imageRepWithCIImage: eine Instanz von NSCIImageRep erstellt und als Repräsentation dem Image hinzugefügt. Jetzt kann das Image im outputImageView angezeigt werden. Auch dieses System der Rückwandlung merken Sie sich bitte. Kompilieren und starten Sie die Anwendung. Im Imageview sollte ein Bild zu sehen sein. Beim Ändern der Werte der Slider sollte das Bild entsprechend den Einstellungen angezeigt werden. Gratulation. Sie haben soeben Ihre erste Core ImageAnwendung geschrieben.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Color Controls 3« von der Webseite herunterladen.
Hintergründe: Wieso Core Image so schnell ist Vielleicht wundern Sie sich ein wenig über die Geschwindigkeit, mit der Core Image Bilder manipulieren kann. Für die gute Performanz von Core Image sind im Grunde zwei Tatsachen verantwortlich. Core Image nutzt intensiv Lazy-Evaluation Unter Lazy-Evaluation wird das möglichst lange Hinauszögern von Berechnungen verstanden. Falls die Eingabeparameter eines Filters geändert werden, hat dies nicht unbedingt zur Folge, dass Core Image den Filter nutzt, um das Ausgabebild zu berechnen. Dies geschieht erst, sobald das Ausgabebild wirklich gezeichnet werden soll. Sie fragen sich jetzt vielleicht, welche Vorteile dieses Hinauszögern der Berechnung hat. Irgendwann müssen die Berechnungen doch erzwungenermaßen durchgeführt werden. Ob nun jetzt oder einige Sekunden später, ist doch egal. Aber stellen Sie sich vor, Sie kombinieren den Filter CIColorControls mit dem Filter CILanczosScaleTransform, um den Kontrast eines Bildes zu erhöhen und es dann mit CILanczosScaleTransform kleiner zu machen. 450
Kapitel 5
Core Image und Core Animation
CIColorControls
CILanczosScale Transform
Optimierungsbedarf: Zunächst wird der Filter CIColorControls und anschließend der Filter CILanczosScaleTransform angewandt.
Sie erzeugen zunächst einen CIColorControls-Filter und übergeben das Ausgabebild dann an den CILanczosScaleTransform-Filter, der das Bild nun kleiner macht. Das Ganze sieht auf den ersten Blick sehr vernünftig aus. Sobald Sie das kleine Bild zeichnen möchten, optimiert Core Image allerdings den Ablauf.
CILanczosScale Transform
CIColorControls
Automatisch optimiert: Core Image hat den schlechten Ablauf erkannt und behoben. Zunächst wird das Bild kleinskaliert, um dann den CIColorControls-Filter anzuwenden.
451
SmartBooks
Objective-C und Cocoa – Band 2
Core Image macht das Bild zunächst kleiner, um dann den CIColorControls-Filter anzuwenden. Da der CIColorControls-Filter kleinere Bilder schneller bearbeiten kann als große, ist dies natürlich sinnvoll. Core Image nutzt, wann immer möglich, zur Berechnung die GPU (Graphics Processing Unit) Die GPU ist der Prozessor auf der Graphikkarte, der auf Graphikberechnung spezialisiert ist. Wird Core Image aufgefordert, ein Bild zu zeichnen, auf das ein oder mehrere Filter angewandt werden sollen, so führt Core Image die dazu notwendigen Berechnungen in der GPU durch, was in der Regel deutlich schneller ist als die Durchführung der Berechnungen in der CPU. Core Image ist allerdings in der Lage, Berechnungen auch in der CPU durchzuführen, falls keine GPU verfügbar ist.
Mehrere Filter kombinieren Mehrmals wurde bisher erwähnt, dass es möglich ist, mehrere Filter zu kombinieren. Es gibt im Grunde zwei Möglichkeiten, mehrere Filter zu kombinieren. Wir werden Ihnen beide zeigen.
Composite-Operation-Filter Core Image bringt die Kategorie CICategoryCompositeOperation mit, die Filter enthält, mit denen ein Bild in gewisser Weise über ein anderes Bild gelegt werden kann. Das Prinzip der Überlagerung zweier Bilder sollte Ihnen schon vom Zeichen von Instanzen der Klasse NSImage bekannt sein. Denn beim Zeichnen einer Instanz von NSImage müssen Sie auch eine Compositing-Operation angeben. Die einfachste Composite-Operation legt ein Bild über ein anderes. Diese wird durch einen Filter namens CISourceOverCompositing umgesetzt. Mit diesem Filter kann man zwei Filter kombinieren. Die folgende Abbildung visualisiert den Ablauf einer Komposition zweier Filter durch Nutzung des Source-Over-Compositing-Filters. Die Abbildung zeigt, wie mit dem Filter Checkerboard-Generator ein Schachbrett erzeugt wird, auf das dann mit dem Filter Sunbeams-Generator erzeugte Sonnenstrahlen gelegt werden. Das durch die Kombination der beiden Filter erzeugte beziehungsweise beschriebene Bild könnte wieder mit anderen Filtern kombiniert werden. So lassen sich mit Instanzen der Klasse CIFilter höchst komplexe, baumartige Strukturen beschreiben, die beliebige Bilder manipulieren können.
452
Kapitel 5
Core Image und Core Animation
CICheckerboardGenerator
CISunbeamsGenerator
CISourceOverCompositing
Kombination durch Überlagerung: Die Sonnenstrahlen werden auf ein Schachbrett gelegt.
Der in der obigen Abbildung visualisierte Ablauf soll nun in diesem Beispiel mit Core Image implementiert werden. In diesem Beispiel werden Sie lernen, wie Sie zwei Filter durch Überlagerung kombinieren, die Klasse CIColor nutzen, was ein Core Image Context ist, wie Sie diesen erzeugen und wie Sie ein CIImage zeichnen. Erzeugen Sie ein neues Projekt SourceOverCompositing vom Typ Cocoa Application. Fügen Sie bitte das Quartz-Core-Framework zum Projekt hinzu. Die Instanz der Klasse SourceOverCompositingAppDelegate soll das Resultat, also das Aus453
SmartBooks
Objective-C und Cocoa – Band 2
gabebild, der Kombination der Filter Checkerboard-Generator und Sunbeams-Generator, in einem Imageview anzeigen. Legen Sie daher im Interface der Klasse SourceOverCompositingAppDelegate ein Outlet auf ein Imageview an. #import @interface SourceOverCompositingAppDelegate : NSObject { IBOutlet NSImageView *imageView; } @end
Öffnen Sie nun die Datei MainMenu.xib. Ziehen Sie ein Imageview auf das Fenster. Verbinden Sie anschließend das Outlet imageView der Instanz von Source OverCompositingAppDelegate mit dem eben angelegten ImageView. Speichern und schließen Sie die Datei MainMenu.xib und kehren Sie zu Xcode zurück. Öffnen Sie die Datei SourceOverCompositingAppDelegate.m. In der Methode -awake FromNib sollen nun im ersten Schritt das Schachbrett und die Sonnenstrahlen erzeugt werden. #import "SourceOverCompositingAppDelegate.h" #import @implementation SourceOverCompositingAppDelegate - (void)awakeFromNib { CIFilter *checkerboardFilter = [CIFilter filterWithName:@"CICheckerboardGenerator"]; [checkerboardFilter setDefaults]; …
Durch den Aufruf der Klassenmethode +filterWithName: wird der Filter erzeugt, der das Schachbrett generieren wird. Der Filter CICheckerboardGenerator verfügt über einige Eingabeparameter, mit denen das Schachbrett konfiguriert werden kann. Die Eingabeparameter inputColor0 und inputColor1 erlauben, die Farben der Quadrate des Schachbrettes festzulegen. Farben werden im Zusammenhang mit Core Image durch Instanzen der Klasse CIColor beschrieben: … CIColor* inputColor0 = [CIColor colorWithRed:0.0f green:0.0f blue:0.0f];
454
Kapitel 5
Core Image und Core Animation
CIColor* inputColor1 = [CIColor colorWithRed:0.5f green:0.5f blue:0.5f]; [checkerboardFilter setValue:inputColor0 forKey:@"inputColor0"]; [checkerboardFilter setValue:inputColor1 forKey:@"inputColor1"]; …
Hinter dem Eingabeparameter inputWidth verbirgt sich die Breite der einzelnen Felder des Schachbrettes: … [checkerboardFilter setValue:[NSNumber numberWithFloat:25.0f] forKey:kCIInputWidthKey]; …
Jetzt ist die Beschreibung des CICheckerboardGenerator-Filters vollständig. Nun wird der Filter zur Erzeugung der Sonnenstrahlen angelegt. … CIFilter *sunbeamsFilter = [CIFilter filterWithName:@"CISunbeamsGenerator"]; [sunbeamsFilter setDefaults]; …
Der Filter CISunbeamsGenerator hat zahlreiche Eingabeparameter, allerdings haben die Standardwerte der Eingabeparameter für unser Vorhaben optimale Werte. Daher müssen die Werte der Eingabeparameter nicht weiter angegeben werden. Jetzt ist es an der Zeit, die Sonnenstrahlen auf das Schachbrett zu legen. Wie schon angedeutet, kann dazu der Filter CISourceOverCompositing benutzt werden, der wie jeder andere Filter auch durch Angabe seines Namens erzeugt werden kann. … CIFilter *sourceOverFilter = [CIFilter filterWithName:@"CISourceOverCompositing"]; [sourceOverFilter setDefaults]; …
455
SmartBooks
Objective-C und Cocoa – Band 2
Ein CISourceOverCompositing-Filter verfügt genau über zwei Eingabeparameter: Zum einen über den Eingabeparameter namens inputBackgroundImage, welcher ein Bild festlegt, auf das ein weiteres Bild gelegt werden soll. Der andere Eingabeparameter heißt inputImage und legt das Bild fest, welches über das Input-Background-Image gelegt wird: … CIImage* backgroundImage = [checkerboardFilter valueForKey:kCIOutputImageKey]; [sourceOverFilter setValue:backgroundImage forKey:kCIInputBackgroundImageKey]; CIImage* inputImage = [sunbeamsFilter valueForKey:kCIOutputImageKey]; [sourceOverFilter setValue:inputImage forKey:kCIInputImageKey]; …
Zunächst wird das Ausgabebild des Schachbrettfilters ermittelt, welches dann zum Wert des Eingabeparameters inputBackgroundImage wird. Anschließend wird das Ausgabebild des Sonnenstrahlenfilters ermittelt, welches dann zum Wert des Eingabeparameters inputImage wird. Somit teilen wir dem CISourceOverCompositing-Filter mit, dass die Sonnenstrahlen über das Schachbrett gelegt werden sollen. … CIImage *outputImage = [sourceOverFilter valueForKey:kCIOutputImageKey]; …
Nun wird das Ausgabebild des CISourceOverCompositing-Filters ermittelt, welches im ImageView angezeigt werden soll. Wie Sie wissen, kann ein CIImage nicht direkt in einem ImageView angezeigt werden. Aber wieso ist das so? Bei Instanzen der Klasse CIImage handelt es sich nicht wirklich um Bilder im herkömmlichen Sinne. Stellen Sie sich eine Instanz von CIImage eher wie eine Anleitung vor, die beschreiben kann, wie das Bild erzeugt wird. Ein CIImage weiß folglich, wie es ein Bild erzeugen kann. Wir wollen jetzt das Ausgabebild (Variable namens output Image) auf ein gewöhnliches Image zeichnen, um dann das Image im ImageView anzuzeigen. Dazu werden insbesondere zwei Dinge benötigt: eine Instanz von NS Image und ein Core Image Context, der es erlaubt, ein CIImage zu zeichnen.
456
Kapitel 5
Core Image und Core Animation
… NSSize size = [imageView frame].size; NSImage* image = [[[NSImage alloc] initWithSize:size] autorelease]; …
Die Größe der Instanz von NSImage entspricht jetzt der des ImageViews. Zur Ermittlung eines Core-Image-Kontextes (also einer Instanz von CIContext) wird zunächst eine Instanz von NSGraphicsContext benötigt. Durch den Aufruf von -lockFocus wird automatisch ein entsprechender NSGraphicsContext erzeugt. Anschließend kann dieser Kontext durch den Aufruf der Methode -CIContext veranlasst werden, einen Core Image Kontext zu liefern. … [image lockFocus]; NSGraphicsContext* cocoaContext = [NSGraphicsContext currentContext]; CIContext* ciContext = [cocoaContext CIContext]; …
Nun kann in diesen Core Image-Kontext das Ausgabebild gezeichnet werden. … NSRect imageRect = NSZeroRect; imageRect.size = size; [ciContext drawImage:outputImage atPoint:CGPointZero fromRect:imageRect]; …
Anschließend wird der Fokus der Instanz von NSImage wieder freigegeben, und das erzeugte Bild im ImageView kann angezeigt werden. … [image unlockFocus]; imageView.image = image; } @end
Kompilieren und starten Sie die Anwendung. Sie werden ein Schachbrett sehen, über das die erzeugten Sonnenstrahlen gelegt wurden.
457
SmartBooks
Objective-C und Cocoa – Band 2
HILFE Sie können das Projekt in diesem Zustand als Projekt »SourceOverCompositing« von der Webseite herunterladen. Dies ist eine einfache Möglichkeit, zwei Filter zu kombinieren. Natürlich wäre es auch möglich gewesen, dies ohne einen CISourceOverCompositing-Filter zu erreichen, indem das Schachbrett und die Sonnenstrahlen nacheinander in ein Bild gezeichnet worden wären. Allerdings würden Sie dann auf etwaige Optimierungen seitens Core Image verzichten und Sie hätten keine Instanz von CIImage, welche das repräsentiert, was Sie eigentlich wollen. Weitere Kombinationen wären dann nicht mehr möglich.
Filter verketten Neben der Möglichkeit, mehrere Filter durch Überlagerung zu kombinieren, gibt es noch eine weitere: Filter können verkettet werden. Das Prinzip der Verkettung mehrerer Filter ist recht einfach. Die meisten Filter erwarten ein Eingabebild, welches dann manipuliert wird. Als Resultat der Manipulation wird das Ausgabebild bezeichnet. Filter, die ein Eingabebild erwarten und ein Ausgabebild liefern, lassen sich verketten. Hierbei wird das Ausgabebild eines Filters zum Eingabebild eines weiteren Filters gemacht. Diese Verkettung kann beliebig fortgesetzt werden. Die folgende Abbildung zeigt eine einfache Verkettung zweier Filter.
inputImage
CIDotScreen
outputImage
inputImage CIAffineTile outputImage
Verkettung zweier Filter durch Verbinden des Ausgabebildes mit dem Eingabebild
458
Kapitel 5
Core Image und Core Animation
Ein beliebiges Bild dient als initiales Eingabebild eines Filters. Der Name des Parameters des Eingabebildes ist in der Regel inputImage. In der obigen Abbildung wird auf das initiale Eingabebild der CIDotScreen-Filter angewandt. Das Ausgabebild wird dann zum Eingabebild des CIAffineTile-Filters. Der in der obigen Abbildung visualisierte Ablauf soll nun in diesem Beispiel mit Core Image implementiert werden. Zunächst soll auf ein beliebiges Bild der Filter CIDotScreen angewandt werden. Der CIDotScreen-Filter legt ein punktförmiges Raster auf sein Eingabebild. Den dadurch erzielten Effekt kennen Sie vielleicht von Bildern in Zeitungen. Anschließend soll auf das Ausgabebild des CIDotScreen-Filters der Filter CIAffineTile angewandt werden. Der Filter CIAffineTile wendet eine beliebige affine Transformation auf sein Eingabebild an und kachelt dieses dann. Das entstandene Ausgabebild soll dann in einem ImageView angezeigt werden. Erzeugen Sie ein neues Projekt Two Filters Concat vom Typ Cocoa Application. Fügen Sie bitte das Quartz-Core-Framework zum Projekt hinzu. Die Instanz der Klasse Two_Filters_ ConcatAppDelegate soll das Resultat, also das Ausgabebild, der Verkettung der Filter Dot-Screen und Affine-Tile, in einem Imageview anzeigen. Legen Sie daher im Interface der Klasse Two_Filters_ConcatAppDelegate ein Outlet auf ein Imageview an. #import @interface Two_Filters_ConcatAppDelegate : NSObject { IBOutlet NSImageView *imageView; } @end
Öffnen Sie nun die Datei MainMenu.xib. Ziehen Sie ein Imageview auf das Fenster. Verbinden Sie anschließend das Outlet imageView der Instanz von Two_Filters_ ConcatAppDelegate mit dem eben angelegten Imageview. Speichern und schließen Sie die Datei MainMenu.xib und kehren Sie zu Xcode zurück. Ziehen Sie ein beliebiges Bild in den Ordner namens Resources in der Projektleiste von Xcode. Nennen Sie das Bild me. Dieses Bild wird als initiales Eingabebild dienen. Öffnen Sie die Datei Two_Filters_ConcatAppDelegate.m. #import "Two_Filters_ConcatAppDelegate.h" #import @implementation Two_Filters_ConcatAppDelegate - (void)awakeFromNib { CIFilter *dotScreenFilter = [CIFilter filterWithName: @"CIDotScreen"];
459
SmartBooks
Objective-C und Cocoa – Band 2
[dotScreenFilter setDefaults]; NSImage *image = [NSImage imageNamed:@"me"]; NSData *imageData = [image TIFFRepresentation]; CIImage *inputImage = [CIImage imageWithData:imageData]; [dotScreenFilter setValue:inputImage forKey:kCIInputImageKey]; …
Zunächst wird in der Methode -awakeFromNib ein Dot-Screen-Filter erzeugt. Durch den Aufruf der Klassenmethode +imageNamed: wird ein Image erstellt, dessen Daten dann durch die Methode -TIFFRepresentation ermittelt werden. Anhand dieser Daten wird das Eingabebild des CIDotScreen-Filters erzeugt. … CIFilter* affineTileFilter = [CIFilter filterWithName:@"CIAffineTile"]; [affineTileFilter setDefaults]; CIImage* dotScreenOutputImage = [dotScreenFilter valueForKey:kCIOutputImageKey]; [affineTileFilter setValue:dotScreenOutputImage forKey:kCIInputImageKey]; …
Nun wird der Affine-Tile-Filter erzeugt, und das Ausgabebild des Dot-Screen-Filters wird zum Eingabebild des Affine-Tile-Filters. Der Affine-Tile-Filter hat einen interessanten Eingabeparameter. Mit dem Eingabeparameter namens inputTransform ist es möglich, eine Instanz der Klasse NSAffineTransform anzugeben, die die affine Transformation des Affine-Tile-Filters beschreibt. … NSAffineTransform *transform = [NSAffineTransform transform]; [transform rotateByDegrees:45.0f]; [transform scaleBy:0.3f]; [affineTileFilter setValue:transform forKey:kCIInputTransformKey]; …
460
Kapitel 5
Core Image und Core Animation
Die erzeugte affine Transformation dreht das Eingabebild um 45 Grad und skaliert es um den Faktor 0.3. Wie im vorherigen Beispiel wird nun das Ausgabebild ermittelt und in ein Image gezeichnet. … CIImage* outputImage = [affineTileFilter valueForKey:kCIOutputImageKey]; NSSize size = [imageView frame].size; NSImage* image = [[[NSImage alloc] initWithSize:size] autorelease]; [image lockFocus]; NSGraphicsContext* cocoaContext = [NSGraphicsContext currentContext]; CIContext* ciContext = [cocoContext CIContext]; NSRect imageRect = NSZeroRect; imageRect.size = size; [ciContext drawImage:outputImage atPoint:CGPointZero fromRect:imageRect]; [image unlockFocus]; [imageView setImage:image]; } @end
Kompilieren und starten Sie die Anwendung.
HILFE Sie können das Projekt in diesem Zustand als »TwoFilters Concat« von der Webseite herunterladen.
Fazit und Ausblick Dieses Kapitel hat Ihnen einen ersten Einblick in Core Image gegeben. Sie sind jetzt in der Lage, Filter zu erzeugen, zu verbinden und das resultierende Bild zu zeichnen. Außerdem wurden Ihnen die Zusammenhänge zwischen Core Image
461
SmartBooks
Objective-C und Cocoa – Band 2
und Core Animation näher gebracht. Mit diesem Wissen können Sie Ihre Anwendungen mit nur wenigen Zeilen Quellcode aufpeppen. Filter-Generator Sollten Sie sich noch intensiver mit Core Image beschäftigen wollen, so könnten Sie sich die Klasse CIFilterGenerator anschauen. Diese Klasse erlaubt es, mehrere Filter miteinander zu verbinden. Eine entsprechend konfigurierte Instanz von CIFilterGenerator können Sie dann mittels der Methode -writeToURL:atomically: abspeichern. Diese Methode erzeugt eine Property-List und speichert diese unter der angegeben URL ab. Sie können der erzeugten Datei die Dateiendung cifilter geben und diese Datei in das Verzeichnis ~/Library/Graphics/Image Units legen. Dann können Sie diesen Filter wie gewohnt mit der Klasse CIFilter unter Angabe des entsprechenden Namens laden. Dies ist eine sehr einfache Möglichkeit, verkettete Filter in Core Image verfügbar zu machen. Eigene Filter mit der OpenGL Shading Language Natürlich können Sie auch komplett neue Filter erstellen. Dazu müssen Sie sich ein wenig in die OpenGL Shading Language einarbeiten. Diese Programmiersprache ist für Sie als Kenner von Objective-C recht schnell zu erlernen. Objective-C baut auf der Programmiersprache C auf, und die OpenGL Shading Language ist C-ähnlich. Die grundlegenden Konzepte der OpenGL Shading Language sind Ihnen also schon vertraut. Allerdings werden Sie selten die Notwendigkeit verspüren, eigene Filter zu entwickeln, da die über einhundert Filter, die Core Image mitbringt, sehr viel abgedeckt wird. Durch Kombination dieser über einhundert Filter bleiben fast keine Wünsche offen. Core Image und das ImageKit In OS X 10.5 wurde das ImageKit vorgestellt. Das ImageKit ist ein Framework, welches sich Core Animation, Core Image und AppKit bedient, um Aufgaben, die im Zusammenhang mit Bildern stehen, zu lösen. Die Klasse IKFilterBrowserPanel ist Teil des ImageKits. Mit dieser Klasse können Sie sehr einfach ein Fenster anzeigen, welches einem Benutzer erlaubt, einen Core Image-Filter auszuwählen. Im ersten Beispiel dieses Kapitels haben Sie für den Filter Color-Controls drei Slider erzeugt, um die Eingabeparameter des Filters manipulieren zu können. Eine Instanz von CIFilter ist allerdings in der Lage, ein View zu erzeugen, welches es erlaubt, die Eingabeparameter dieses Filters zu manipulieren. Diese zusätzliche Funktionalität wird vom ImageKit in Form einer Kategorie der Klasse CIFilter bereitgestellt. Durch den Aufruf der Methode -viewForUIConfiguration:excludedKeys: liefert ein CIFilter eine Instanz der Klasse IKFilterUIView, welche Controls enthält, mit denen die Eingabeparameter des Filters manipuliert werden können.
462
Kapitel 5
Core Image und Core Animation
Core Animation Apple beschreibt Core Animation als eine Sammlung von Objective-C-Klassen zur Graphikerzeugung, Graphikprojektierung und für Animationen. Core Animation erschien zuerst für die Desktop-Plattform, wurde allerdings für das iPhone entwickelt. Aus pragmatischer Sicht ist Core Animation ein Hilfsmittel zur Lösung vieler Probleme, die sich auf die Benutzerschnittstelle beziehen. Vor einigen Jahren genügte es, dass eine Cocoa-Anwendung zuverlässig funktionierte und dem Benutzer einen Mehrwert brachte. Es hat sich allerdings gezeigt, dass Anwendungen, die gut funktionieren, sich gleichzeitig gut bedienen lassen und zusätzlich gut aussehen, von Benutzern oft gegenüber rein funktional orientierten Anwendungen bevorzugt werden. Core Animation hilft Ihnen, aus einer funktionalen Anwendung eine Anwendung zu machen, die deren Benutzer staunen lässt. Probleme, die Core Animation löst Core Animation löst insbesondere die Probleme, die in der Vergangenheit von vielen Entwicklern auf unterschiedlichste Art und Weise zu lösen versucht wurden: • Ein View langsam ausblenden • Ein View von A nach B animieren • Auf ein View Core Image-Filter anwenden • Ein Interface ähnlich dem von Front Row implementieren
Der Animator eines Views Mit »Animieren einer Eigenschaft« wird das Verändern des Wertes der Eigenschaft über einen gewissen Zeitraum hinweg bezeichnet. Lassen Sie uns nun zur Tat schreiten und Core Animation an einem View ausprobieren. Anhand dieses Beispiels werden Sie lernen, wie Eigenschaften von Views animiert werden können. Erzeugen Sie mit Xcode ein neues Cocoa-Projekt namens ViewAnimation. Eine Instanz von ViewAnimationAppDelegate soll ein ImageView langsam von A nach B bewegen. Daher benötigt ViewAnimationAppDelegate ein Outlet auf ein Imageview. Das ImageView soll, sobald der Benutzer auf einen Button geklickt hat, von A nach B bewegt werden. Dies bedeutet, dass ViewAnimationAppDelegate eine IBAction benötigt, die Sie am besten -animateImageView: nennen. Das Interface von ViewAnimationAppDelegate sieht wie folgt aus. #import @interface ViewAnimationAppDelegate : NSObject
463
SmartBooks
Objective-C und Cocoa – Band 2
{ NSWindow *window; NSImageView *imageView; } @property (assign) IBOutlet NSImageView *imageView; @property (assign) IBOutlet NSWindow *window; - (IBAction)animateImageView:(id)sender; @end
Wechseln Sie zur Datei ViewAnimationAppDelegate.m. Implementieren Sie die Actionmethode animateImageView:. Ihr Quellcode sollte wie folgt aussehen: #import "ViewAnimationAppDelegate.h" @implementation ViewAnimationAppDelegate @synthesize window, imageView; - (IBAction)animateImageView:(id)sender { NSPoint newOrigin = NSMakePoint(100.0f, 100.0f); [[self imageView] setFrameOrigin:newOrigin]; } @end
Öffnen Sie nun die Datei MainMenu.xib. Klicken Sie die Fensterinstanz doppelt an, so dass das Fenster im Interface Builder angezeigt wird. Öffnen Sie zusätzlich noch die Media-Library und ziehen Sie das vorgefertigte Bild NSAdvanced in die linke untere Ecke des Fensters. Erzeugen Sie noch einen Button mit der Beschriftung animiere ImageView und positionieren Sie den Button in der linken oberen Ecke des Fensters. Verbinden Sie die Action des Buttons mit der Actionmethode animateImageView: der ViewAnimationAppDelegate-Instanz und verbinden Sie das Outlet imageView der ViewAnimationAppDelegate-Instanz mit dem Imageview auf dem Fenster. Ihr User-Interface sollte wie folgt aussehen.
464
Kapitel 5
Core Image und Core Animation
-animateImageView:
App Controller imageView
Der AppController kennt das ImageView und verfügt über die Methode -animateImageView:.
Kompilieren und starten Sie das Programm. Sobald Sie auf den Button klicken, verschiebt sich das Imageview um 100 Punkte nach rechts und um 100 Punkte nach oben. Dies war zu erwarten.
HILFE Sie können das Projekt in diesem Zustand als »ViewAnimation (ohne Animator)« von der Webseite herunterladen. Das Ziel ist es nun, das imageView von (0, 0) nach (100, 100) zu animieren. Um dieses Ziel zu erreichen, müssen Sie lediglich eine einzige Zeile minimal verändern. Passen Sie die Methode animateImageView: wie folgt an: - (IBAction)animateImageView:(id)sender { NSPoint newOrigin = NSMakePoint(100.0f, 100.0f); [[[self imageView] animator] setFrameOrigin:newOrigin]; }
HILFE Sie können das Projekt in diesem Zustand als »ViewAnimation (mit Animator)« von der Webseite herunterladen. Kompilieren und starten Sie das Programm. Klicken Sie wieder auf den Button und Sie werden sehen, dass das Image-View seine Position von (0, 0) nach (100, 100) 465
SmartBooks
Objective-C und Cocoa – Band 2
animiert. Erstaunlich! Sie fragen sich nun sicherlich, wieso und wie das funktioniert. Dies funktioniert, da die Nachricht -setFrameOrigin: nicht direkt an das Imageview, sondern an den Animator (self.imageView.animator) des Imageviews gesandt wird. Der Animator des Imageviews ist ein Stellvertreterobjekt, welches das Animatable-Property-Container-Protokoll implementiert (Band I, S. 413). Bevor das, was im Hintergrund beim Senden der Nachricht -setFrameOrigin: an den Animator geschieht, im Detail erklärt wird, sollten Sie versuchen, auch andere Eigenschaften des Imageviews zu animieren. Versuchen Sie zum Beispiel, die Größe des Imageviews zu verdoppeln. Eine Möglichkeit, dies zu erreichen, wäre die folgende. - (IBAction)animateImageView:(id)sender { NSSize newSize = [[self imageView] frame].size; newSize.width *= 2.0f; newSize.height *= 2.0f; [[[self imageView] animator] setFrameSize:newSize]; }
Die Breite und Höhe wird mit 2.0 multipliziert, was einer Verdoppelung der Größe des Imageviews entspricht. Anschließend wird die Größe des Image-Views wieder durch den Animator gesetzt.
Das Protokoll NSAnimatablePropertyContainer Die Magie, die hinter dem Animator eines Views steckt, ist recht simpel: Beim Aufruf von -setFrameSize: geschieht Folgendes: Core Animation ruft -animations auf. Die Methode -animations liefert ein Dictionary, dessen Schlüssel meist Eigenschaften des Views und dessen Werte Animationsobjekte sind. Standardmäßig liefert -animations nil. Wäre -animations nicht nil, so würde Core Animation prüfen, ob in -animations ein Schlüssel frameSize existiert. Falls dem so wäre, würde Core Animation das Objekt nutzen, also ein Animationsobjekt, welches zu dem Key frameSize gehört, um die Animation durchzuführen. Ist -animations gleich nil oder enthält keinen Schlüssel frameSize, so ruft Core Animation +defaultAni mationForKey: auf und übergibt als Schlüssel frameSize. +defaultAnimationForKey: liefert ein zum übergebenen Schlüssel passendes Animationsobjekt oder nil, für die zu dem übergebenen Schlüssel gehörende Eigenschaft keine Animation durchgeführt werden soll. Um das zum Schlüssel gehörende Animationsobjekt zu bestimmen, konsultiert die Klassenmethode +defaultAnimationForKey: die Methode -animationForKey: . Vielleicht wird Ihnen jetzt klar, wieso wir mit dem Animator des Imageviews kommunizieren und nicht direkt mit dem Imageview. Das 466
Kapitel 5
Core Image und Core Animation
Animatable-Property-Container-Protokoll kann prinzipiell von jeder Klasse implementiert werden. Durch das Implementieren des Animatable-Property-Container-Protokolls können zu einer bestehenden Klasse Animationen hinzugefügt werden, aber auch bereits vorhandene Animationen modifiziert oder entfernt werden.
Die Animationsklassen Bisher sprachen wir ganz abstrakt von Animationsobjekten. Sie werden nun die von Core Animation bereitgestellten Animationsklassen kennenlernen. Diese Animationsklassen werden zum Beschreiben von Animationen benutzt. In den folgenden Abschnitten werden die Animationsklassen kurz erklärt. CAAnimation CAPropertyAnimation
CABasicAnimation
CATransition
CAAnimationGroup
CAKeyframeAnimation
Klassendiagramm der Animationsklassen
Die Klasse CAAnimation CAAnimation ist eine abstrakte Basisklasse, die das CA-Media-Timing-Protokoll und das CA-Action-Protokollgrundlegend implementiert. Zusätzlich dazu hat CAAnimation einige interessante Eigenschaften. Sie können beispielsweise eine Funktion (eine Instanz von CAMediaTimingFunction) festlegen, die den zeitlichen Ablauf der Animation regelt. Außerdem können Sie pro Animation ein Delegate festlegen, welches über Start und Ende der Animation informiert wird. Die Klasse CATransition CATransition leitet sich von CAAnimation ab. Instanzen von CATransition beschreiben Animationen, die in Form von Übergängen realisiert werden. Denken Sie hierbei an die in Keynote üblichen Übergangseffekte. Mit CATransition können mit -setType: und setSubtype: sehr einfache Übergänge beschrieben werden. Allerdings ist es möglich, mit -setFilter: einen fast beliebigen Core Image-Filter zu nutzen, um so sehr komplexe Übergänge zu beschreiben. Die Klasse CAPropertyAnimation CAPropertyAnimation leitet von CAAnimation ab und ist eine abstrakte Subklasse von CAAnimation. Die Klasse CAPropertyAnimation ist fest mit einer Eigenschaft 467
SmartBooks
Objective-C und Cocoa – Band 2
(Property) durch die Angabe eines Schlüsselpfades verbunden. Den Schlüsselpfad teilen Sie CAPropertyAnimation gleich bei der Erzeugung durch die Verwendung von +animationWithKeyPath: mit. Mit einem CAPropertyAnimation-Objekt können Sie folglich eine Eigenschaft animieren. Die Klasse CABasicAnimation CABasicAnimation leitet von CAPropertyAnimation ab. Die Klasse CABasicAnimation bezieht sich auch auf eine Eigenschaft, lässt Sie aber zusätzlich einen Startwert (fromValue), Endwert (toValue) und einen Zwischenwert (byValue) festlegen. Zu beachten ist hierbei, dass alle Werte optional sind und nicht mehr als zwei Werte ungleich nil sein dürfen. Die Klasse CAKeyframeAnimation CAKeyframeAnimation leitet von CAPropertyAnimation ab. Die Klasse CAKey frameAnimation bezieht sich auch auf eine Eigenschaft, lässt Sie aber zusätzlich einen Pfad (path) oder ein Array mit Werten (values) angeben. Nützlich ist eine Keyframe-Animation zum Beispiel, wenn Sie einen Layer um 360 Grad drehen möchten. Core Animation geht bei einer Animation immer den kürzesten Weg. Bei einer Drehung um 360 Grad ist es aus Sicht von Core Animation das Beste, einfach nichts zu tun, da der Layer vor und nach der Drehnung die gleiche Position und Lage haben würde. Mit einer Keyframe-Animation ist es durch Angabe von Zwischenschritten möglich, dennoch einen Layer um 360 Grad zu drehen. Die Klasse CAAnimationGroup CAAnimationGroup leitet sich von CAAnimation ab. Instanzen von CAAnimation Group beschreiben, wie der Name schon vermuten lässt, eine Animation, die sich aus mehreren anderen Animationen zusammensetzt.
Die Layerklassen Blicken Sie auf das, was Sie bisher über Core Animation kennengelernt haben, zurück, so werden Sie sich vielleicht fragen, ob das denn schon alles gewesen ist. Schließlich war es ohne Core Animation bisher auch möglich, Eigenschaften von Views zu animieren. Es musste zwar etwas mehr Arbeit investiert werden, aber im Prinzip ist das Animieren von Views keine wirkliche Neuerung. Core Animation kann aber mehr, denn es wurden mit Core Animation noch eine Vielzahl weiterer Klassen eingeführt: die Layerklassen. Die Layerklassen lösen unter anderem ein großes Problem. Als Sie mittels Core Animation die Eigenschaften des ImageViews animierten, hat sich das Image-View währenddessen einige Male neu gezeichnet. Das Zeichnen ist oftmals ein sehr rechenintensiver Prozess. Bei der Animation einzelner Views mag dies nicht ins Gewicht fallen, sobald Sie allerdings 468
Kapitel 5
Core Image und Core Animation
versuchen, mit der vorgestellten Technik mehrere Views gleichzeitig zu animieren, werden Sie mit Sicherheit auf Performanceprobleme stoßen. Die Animationen ruckeln. Genau dieses Problem wird durch die Layerklassen gelöst. In vielen Fällen ist ein Neuzeichnen des Inhaltes eines Views unnötig. Zum Beispiel dann, wenn ein View lediglich seine Position verändert. Core Animation bzw. die Layerklassen lösen das Problem unter anderem dadurch, dass das Neuzeichnen auf ein Minimum reduziert wird. Layer besitzen Eigenschaften wie Position, Größe, Hintergrundfarbe und viele mehr. Jeder Layer verfügt wie Views über ein eigenes Koordinatensystem. Die grundlegende Layerklasse ist CALayer. Alle weiteren Layerklassen (CATiledLayer, CATextLayer, CAScrollLayer, QTMovieLayer, ...) leiten (direkt oder indirekt) von CALayer ab. Die Layerklassen werden von Core Animation durch sogenannte Layer-backed-Views in die auf Views basierende Architektur integriert.
Layer-backed-Views Seit Leopard besitzt NSView zwei im Zusammenhang mit Core Animation wichtige neue Eigenschaften: layer und wantsLayer. Standardmäßig ist wantsLayer gleich NO und layer gleich nil. In diesem Zustand ist ein View nicht Layer-backed. Sobald ein View die Nachricht -setWantsLayer: mit YES als Argument erhält, erzeugt es automatisch einen Layer. Views in diesem Zustand werden dann Layer-backed genannt. Wird ein View zu einem Layer-backed-View, so werden alle Subviews automatisch auch layer-backed. Mit Layer-backed-Views können interessante Dinge gemacht werden.
• •
Mit -setAlphaValue: kann die Durchsichtigkeit eines Views verändert werden.
•
Mit -setCompositingFilter: kann ein Core Image-Filter festgelegt werden, der beeinflusst, wie der Inhalt des Views mit seinem Hintergrund gemischt wird.
•
Mit -setBackgroundFilters: können Core Image-Filter festgelegt werden, die auf den Hintergrund des Superviews angewandt werden.
…
Eine Bildershow mit Core Animation Mit dem bisher vermittelten Wissen sind Sie bereits jetzt in der Lage, mit Core Animation wundervolle Dinge zu realisieren. Wir wollen jetzt anhand eines Beispiels das vorhandene Wissen in der Praxis erproben. Sie werden jetzt eine Anwendung entwickeln, die in der Lage ist, eine Reihe von Bildern anzuzeigen. Das Funktionsprinzip der Anwendung lässt sich wie folgt zusammenfassen. 469
SmartBooks
Objective-C und Cocoa – Band 2
Ein Klick auf Weiter schiebt das nächste Bild von rechts in die Mitte des Fensters, und das aktuelle Bild wird nach links aus dem sichtbaren Bereich geschoben. Hingegen bewirkt ein Klick auf Zurück, dass sich das vorherige Bild von links in die Mitte des Fensters und das aktuelle Bild nach rechts aus dem sichtbaren Bereich schiebt.
Der Verlauf der Animation der Bilder-Show
Wie könnte das wohl realisiert sein? Eine Möglichkeit wäre, für jedes Bild, das angezeigt werden kann, eine Instanz von Imageview zu erzeugen und entsprechend zu positionieren. Diese Lösung würde nicht nur recht schlecht skalieren, sondern sie wäre auch recht kompliziert zu implementieren. Beruhigen Sie sich: Die Lösung ist dank Core Animation recht einfach. Lassen Sie uns aber zunächst das Beispiel implementieren. Anschließend können wir uns dann gemeinsam über die Einfachheit freuen. Legen Sie ein neues Projekt der Art Cocoa an. Nennen Sie es am besten Bildershow. Linken Sie das Projekt gegen das QuartzCore-Framework. Erzeugen Sie eine neue Objective-C-Klasse namens ImageShowController. Diese Klasse wird die Bildershow regeln. Um eine Bildershow regeln zu können, muss der Image ShowController wissen, welche Bilder er anzeigen können soll. Diese Information speichert ImageShowController in Form eines Arrays namens images, bestehend aus Objekten der Klasse NSImage. Außerdem muss ImageShowController darüber informiert werden, in welchem View die Bildershow stattfinden soll. Dies wird ihm durch ein Outlet namens imageShowView auf eine Instanz von NSView mitgeteilt. Intern erzeugt der ImageShowController eine Instanz von NSImageView namens currentImageView, welche das aktuelle Bild anzeigt. Darüber hinaus muss Image ShowController noch wissen, welchen Index das aktuell angezeigte Bild hat. Diese Information speichert ImageShowController in einer Variable vom Typ NSUInteger namens indexOfCurrentImage. Schlussendlich verfügt ImageShowController noch über zwei IBActions: -nextImage: veranlasst den Controller, das nächste Bild anzuzeigen, und -previousImage: das vorherige. Insgesamt ergibt sich damit das folgende Interface für ImageShowController:
470
Kapitel 5
Core Image und Core Animation
#import @interface ImageShowController : NSObject { IBOutlet NSView *imageShowView; NSArray *images; NSUInteger indexOfCurrentImage; NSImageView *currentImageView; } @property ( copy ) NSArray *images; @property NSUInteger indexOfCurrentImage; - (IBAction)nextImage:(id)sender; - (IBAction)previousImage:(id)sender; @end
Um eine Bildershow zu erzeugen, muss lediglich eine Instanz von ImageShowController angelegt werden, die durch -setImages: mit denjenigen Bildern versorgt wird, die Teil der Bildershow werden sollen, und die mit -setImageShowView: ein View zugewiesen bekommt, in der die Bildershow stattfinden kann. Bevor wir uns die konkrete Implementierung von ImageShowController zu Gemüte führen, benötigen wir noch eine zweite Klasse, die die Aufgabe hat, eine Bildershow zu erzeugen. Diese Aufgabe übernimmt die Klasse BildershowAppDelegate. Bildershow AppDelegate soll lediglich eine Instanz von ImageShowController mit Bildern versorgen. Es ergibt sich also folgendes Interface für BildershowAppDelegate.h: #import @class ImageShowController; @interface BildershowAppDelegate : NSObject { IBOutlet ImageShowController *imageShowController; } @end
Wie Sie sehen, verfügt die Instanz von BildershowAppDelegate über ein Outlet namens imageShowController vom Typ ImageShowController. Nun ist der Interface Builder an der Reihe. Öffnen Sie die Datei MainMenu.xib. Legen Sie im Interface Builder eine Instanz von ImageShowController und eine Instanz von Bildershow AppDelegate an. Verbinden Sie das Outlet imageShowController der Bildershow AppDelegate-Instanz mit der Instanz von ImageShowController. Nun weiß das 471
SmartBooks
Objective-C und Cocoa – Band 2
BildershowAppDelegate über den ImageShowController Bescheid. Erzeugen Sie anschließend im Fenster der Datei MainMenu.xib zwei Schaltflächen. Beschriften Sie die eine mit Weiter und die andere mit Zurück. Verbinden Sie die Action der Schaltfläche mit der Beschriftung I mit der Action -nextImage: von ImageShowController. Verbinden Sie analog die Action der Schaltfläche mit der Beschriftung Zurück mit der Action -previousImage: von ImageShowController. Positionieren Sie die beiden Schaltflächen wie in der Abbildung am unteren Rand des Fensters. Ziehen Sie nun eine Instanz von NSCustomView auf das Fenster und passen Sie die Größe entsprechend an. Verbinden Sie das imageShowView-Outlet von Image ShowController mit dem gerade eben angelegten Custom-View. Gratulation. Das Interface ist komplett. Lassen Sie uns jetzt zur Implementierung schreiten. Die Implementierung von BildershowAppDelegate ist wesentlich einfacher als die von ImageShowController. Ist ja auch klar, da die das BildershowAppDelegate lediglich die Instanz von ImageShowController mit Bildern versorgen muss. Hier also die Implementierung von BildershowAppDelegate: #import "BildershowAppDelegate.h" #import "ImageShowController.h" @implementation BildershowAppDelegate - (void)awakeFromNib { NSArray *images = [NSArray arrayWithObjects: [NSImage imageNamed:NSImageNameBonjour], [NSImage imageNamed:NSImageNameDotMac], [NSImage imageNamed:NSImageNameComputer], [NSImage imageNamed:NSImageNameFolderBurnable], [NSImage imageNamed:NSImageNameFolderSmart], [NSImage imageNamed:NSImageNameNetwork], nil]; [imageShowController setImages:images]; [imageShowController setIndexOfCurrentImage:0]; } @end
In -awakeFromNib wird zunächst ein Array erzeugt, welches mehrere Instanzen von NSImage enthält. Der Einfachheit halber erzeugen wir hier Bilder mit +image Named: und übergeben von Cocoa definierte Namen (wie zum Beispiel NSImage NameBonjour). Bei den erzeugten Bildern handelt es sich um Bilder, die Teil des Cocoa-Frameworks sind. Sie können natürlich beliebig andere Bilder erzeugen. Das Array wird dann mit -setImages: der Instanz von ImageShowController übergeben. Anschließend wird mit -setIndexOfCurrentImage: das erste Bild angezeigt. Jetzt fehlt noch die Implementierung von ImageShowController, die nicht ganz so 472
Kapitel 5
Core Image und Core Animation
einfach ist. Aber keine Angst, die Implementierung beginnt mit dem Importieren der benötigten Headerdateien. ImageShowController verfügt über die Variable currentImageView, die im Interface allerdings über keine Accessoren verfügt. Dies liegt daran, dass diese Variable von ImageShowController lediglich für interne Zwecke benötigt wird. Da wir innerhalb der Implementierung von ImageShowController auf currentImage View gerne über Accessoren zugreifen möchten, definieren wir in einer privaten Kategorie von ImageShowController currentImageView als eine Eigenschaft. Dem Importieren der entsprechenden Headerdateien folgt unmittelbar die Definition der privaten Kategorie. Anschließend folgt die eigentliche Implementierung von ImageShowController. Die Implementierung der Eigenschaften currentImageView, imageShowView und images werden mit @synthesize automatisch erzeugt. #import "ImageShowController.h" #import @interface ImageShowController() @property (retain) NSImageView *currentImageView; @end @implementation ImageShowController @synthesize currentImageView; @synthesize images; - (NSUInteger)indexOfCurrentImage { return indexOfCurrentImage; } - (void)setIndexOfCurrentImage:(NSUInteger)newIndex { indexOfCurrentImage = newIndex; } - (void)awakeFromNib { } - (IBAction)nextImage:(id)sender { }
473
SmartBooks
Objective-C und Cocoa – Band 2
- (IBAction)previousImage:(id)sender { } @end
Die Eigenschaft indexOfCurrentImage wird von ImageShowController selbst implementiert, da hier die Magie stattfindet. - (void)setIndexOfCurrentImage:(NSUInteger)newIndex { indexOfCurrentImage = newIndex; NSImageView *newImageView = [[[NSImageView alloc] initWithFrame:[[self imageShowView] bounds]] autorelease]; [newImageView setImageFrameStyle:NSImageFrameGrayBezel]; [newImageView setAutoresizingMask:NSViewMinXMargin | NSViewWidthSizable | NSViewMaxXMargin | NSViewMinYMargin | NSViewHeightSizable | NSViewMaxYMargin]; [newImageView setImageScaling: NSImageScaleProportionallyUpOrDown]; NSImage *image = [[self images] objectAtIndex: [self indexOfCurrentImage]]; [newImageView setImage:image]; if([self currentImageView] == nil) { [[imageShowView animator] addSubview:newImageView]; } else { [[imageShowView animator] replaceSubview:[self currentImageView] with:newImageView]; } [self setCurrentImageView:newImageView]; }
474
Kapitel 5
Core Image und Core Animation
Die Methode -indexOfCurrentImage gibt einfach nur den aktuellen Wert zurück, der in indexOfCurrentImage gespeichert ist. Mehr passiert in der Implementierung von -setIndexOfCurrentImage:. Zunächst wird, wie üblich, indexOfCurrentImage mit dem neuen Wert (newIndex) versorgt. Anschließend wird eine Instanz von NSImageView erzeugt und deren Größe so angepasst, dass das erzeugte ImageView das gesamte imageShowView einnimmt. Mit -setImageFrameStyle: wird dem ImageView ein sichtbarer Rand spendiert. Der Aufruf von -setAutoresizingMask: bewirkt, dass sich beim Ändern der Größe des Fensters die Größe des ImageViews automatisch anpasst. Da wir die gezeigten Bilder in ihrer vollen Pracht genießen wollen, teilen wir dem ImageView mittels -setImageScaling: mit, dass es sein Bild so skalieren soll, dass es in das ImageView passt. Das Array -images enthält alle Bilder, die die Instanz von ImageShowController anzeigen kann. -indexOfCurrentImage liefert den Index des Bildes, welches angezeigt werden soll oder aktuell angezeigt wird. Die Methode -objectAtIndex: von NSArray liefert das Objekt, das zu dem übergebenem Index gehört. Der Aufruf [[self images] objectAtIndex:[self indexOfCurrentImage]]
ermittelt folglich das Bild, welches als nächstes angezeigt werden soll und welches mittels -setImage: dem neuen ImageView übergeben wird. Bisher war die Implementierung absolut frei von Core Animation. Dies ändert sich jetzt. -currentImage View liefert genau dann nil, wenn die Instanz von ImageShowController noch kein Bild angezeigt hat, also genau beim ersten Aufruf von -setIndexOfCurrentImage:. Ist -currentImageView gleich nil, so teilen wir dem Animator von imageShowView mit, dass er das erzeugte ImageView als Subview zu imageShowView hinzufügen soll. Ist -currentImageView ungleich nil, so teilen wir dem Animator mit -replaceSubView:with: mit, dass er das aktuelle ImageView (-currentImageView) mit dem neu erzeugten ImageView (newImageView) austauschen soll. Am Ende der Implementierung von -setIndexOfCurrentImage: wird noch mit -setCurrentImageView: eine Referenz auf das eben als Subview hinzugefügte ImageView gespeichert. Versuchen Sie sich jetzt in die Situation von Core Animation zu versetzen. In -setIndexOfCurrentImage: teilen wir Core Animation lediglich mit, dass der Vorgang des Hinzufügens (-addSubview:) und der Vorgang des Austauschens (-replaceSubview:with:) animiert werden soll. Woher soll Core Animation nun wissen, wie genau die Animation aussehen soll? Genau das definieren wir jetzt. - (void)awakeFromNib { [self setCurrentImageView:nil]; [imageShowView setWantsLayer:YES]; indexOfCurrentImage = 0; CATransition *transition = [CATransition animation];
475
SmartBooks
Objective-C und Cocoa – Band 2
[transition setDuration:1.0]; [transition setType:kCATransitionPush]; NSDictionary *newAnimations = [NSDictionary dictionaryWithObject:transition forKey:@"subviews"]; [[imageShowView animator] setAnimations:newAnimations]; }
Was passiert hier? Mit -setCurrentImageView: wird currentImageView auf nil gesetzt, da noch kein Bild angezeigt wurde und folglich noch kein ImageView existiert. Im View, in dem die Bildershow stattfinden wird (-imageShowView), benötigen wir einige Features von Core Animation, daher »aktivieren« wir Core Animation mit -setWantsLayer:. Der Wert von indexOfCurrentImage wird standardmäßig auf 0 gesetzt. Jetzt kommt wieder Core Animation ins Spiel. An dieser Stelle wollen wir Core Animation mitteilen, was passieren soll, wenn die Bildershow zum nächsten oder vorherigen Bild wechselt. Intern wird durch das Austauschen von zwei Image Views von einem zum anderen Bild gewechselt. Unsere Aufgabe ist es, Core Animation zu sagen, dass das Austauschen von Subviews genau so animiert wird, dass der Eindruck entsteht, dass das alte ImageView aus dem sichtbaren Bereich und das neue ImageView in den sichtbaren Bereich geschoben wird. Dies kann Core Animation in Form einer Instanz von CATransition mitgeteilt werden. Wir erzeugen also mittels +animation eine Instanz von CATransition. Mit -setDuration: setzen wir die Dauer der Animation, also des Überganges, auf eine Sekunde. Instanzen von CATransition verfügen unter anderem über die Eigenschaft -type. Mögliche Werte für -type sind kCATransitionFade, kCATransitionMoveIn, kCATransitionPush und kCATransitionReveal. Mit setType: legen Sie die grundlegende Art eines Überganges fest. Der Wert kCATransitionPush, der im Beispiel verwendet wird, bestimmt, dass der »alte« Inhalt weggeschoben und durch den neuen ersetzt werden soll. Das ist ja genau das, was erreicht werden soll. Als Nächstes müssen wir Core Animation noch mitteilen, bei welcher Gelegenheit der eben beschriebene Übergang genutzt werden soll. Der Animator des imageShowViews befragt zu gegebener Zeit sein -animations-Dictionary, in dem hinterlegt ist, bei welcher »Aktion« welche Animation genutzt werden soll. Die Schlüssel des Dictionarys sind »Aktionen«, die durch den dazugehörigen Wert beschrieben werden. Der zu einem Schlüssel gehörende Wert ist eine Subklasse von CAAnimation. Mittels + dictionaryWithObject:forKey: wird ein Dictionary namens newAnimations erzeugt. Als Objekt wird die erzeugte Instanz von CATransition und als Schlüssel »subviews« übergeben. Mit -setAnimations: wird anschließend der Animator des imageShowViews über die neuen Einstellungen für die Animationen informiert. Der Schlüssel »subviews« ist etwas Besonderes. Immer dann, wenn das imageShowView ein Subview hinzufügen oder durch ein anderes ersetzen soll, wird nach einer Animation 476
Kapitel 5
Core Image und Core Animation
zum Schlüssel »subviews« gesucht. Der Mechanismus von Core Animation befragt bei der Suche nach der passenden Animation die unterschiedlichsten Stellen, und per Definition wird auch das von -animations gelieferte Dictionary durchsucht. Jetzt fehlt noch die Implementation von -nextImage: und die Implementation von -previousImage:. - (IBAction)nextImage:(id)sender { [[imageShowView animator] setValue:kCATransitionFromRight forKeyPath:@"animations.subviews.subtype"]; if(([self indexOfCurrentImage]) >= ([images count] - 1)) { [self setIndexOfCurrentImage:0]; } else { [self setIndexOfCurrentImage: [self indexOfCurrentImage] + 1]; } }
Zugegeben, diese Implementierung bedarf einiger Erklärung. Erinnern Sie sich an die Beschreibung von CATransition? Jede Instanz von CATransition verfügt unter anderem über die Eigenschaften -type und -subtype. In der Methode -awakeFromNib wurde der erzeugten Animation (CATransition *transition = ...) mit -setType: der Wert kCATransitionPush übergeben. Damit wurde festgelegt, dass ein Subview, welches durch ein anderes ersetzt wurde, aus dem sichtbaren Bereich geschoben wird. Ob das Subview, welches ersetzt wird, nach links, rechts, oben oder unten geschoben wird, ist noch nicht festgelegt. Genau das legt der Wert der Eigenschaft -subtype einer Instanz von CATransition fest. Mögliche Werte für -subtype sind kCATransitionFromRight (neuer Inhalt kommt von rechts), kCATransitionFromLeft (neuer Inhalt kommt von links), kCATransitionFromTop (neuer Inhalt kommt von oben) und kCATransitionFromBottom (neuer Inhalt kommt von unten). Ein Aufruf von -nextImage: soll zur Folge haben, dass das aktuelle Bild nach links geschoben wird und das neue Bild von rechts nachrückt. Der richtige Wert für die Eigenschaft -subtype wäre folglich kCATransitionFromRight. Es ist nun also die Aufgabe, den Wert der Eigenschaft -subtype der Animation, die für die »subviews«Aktion zuständig ist, auf kCATransitionFromRight zu setzen. Der Schlüsselpfad animations.subviews liefert nacheinander zunächst das Dictionary mit den Animationen und dann die Animation für die Aktion subviews. Mit -setValue:forKeyPath: (mit dem Schlüsselpfad animations.subviews.subtype, der sich auf die Eigen477
SmartBooks
Objective-C und Cocoa – Band 2
schaft -subtype der Animation für die »subviews«-Aktion bezieht) wird der Wert auf kCATransitionFromRight gesetzt. Wird nun im imageShowView ein Subview ersetzt, so geschieht dies von nun an durch einen Übergang, der den neuen Inhalt von rechts in das imageShowView schiebt. Um nun das ImageView, welches das aktuelle Bild anzeigt, durch ein neues zu ersetzen, muss lediglich -setIndexOfCurrentImage: mit einem entsprechend neuen Index aufgerufen werden. In der Methode -setIndexOfCurrentImage: geschieht dann die eigentliche Magie. Wie wird der neue Index ermittelt? Ganz einfach: Wird aktuell das letzte verfügbare Bild angezeigt, so ist der Wert von -indexOfCurrentImage gleich [images count] - 1. Ist dies der Fall, so soll einfach das erste Bild, also das mit dem Index Null angezeigt werden. In allen anderen Fällen ist der neue Index gleich dem alten Index plus eins. Schauen Sie sich am besten nochmals die Implementierung von -setIndexOfCurrentImage: an, denn dort geschieht dann die eigentliche Arbeit. Die Implementierung von -previousImage: sieht änlich wie die von -nextImage: aus. - (IBAction)previousImage:(id)sender { [[imageShowView animator] setValue:kCATransitionFromLeft forKeyPath:@"animations.subviews.subtype"]; if(([self indexOfCurrentImage]) == 0) { [self setIndexOfCurrentImage:[[self images] count] - 1]; } else { [self setIndexOfCurrentImage: [self indexOfCurrentImage] - 1]; } }
Der Wert von subtype wird statt kCATransitionFromRight auf kCATransitionFromLeft gesetzt, um das neue Bild von links kommen zu lassen. Wird außerdem aktuell das erste Bild, also das Bild mit dem Index gleich Null, angezeigt, so soll das letzte Bild, also das mit dem Index [[self images] count] – 1, angezeigt werden. In allen anderen Fällen ist der neue Index gleich dem aktuellen Index minus eins. Gratulation. Speichern Sie das Projekt. Kompilieren und starten Sie es anschließend. Klicken Sie einige Male auf Next beziehungsweise Previous. Trauen Sie sich und ändern Sie den Quellcode ein wenig ab, um ein Gefühl für die Funktionsweise von Core Animation zu erhalten.
478
Kapitel 5
Core Image und Core Animation
HILFE Sie können das Projekt in diesem Zustand als Projekt »Bildershow« von der Webseite herunterladen.
Eigene Eigenschaften eines Views animieren Mit Core Animation können Sie Views und Layer animieren. Wird von Animieren gesprochen, so meint man eigentlich die zeitliche Änderung des Wertes von Eigenschaften. Bisher haben Sie bereits vorhandene Eigenschaften animiert, nicht aber eigene. In diesem Abschnitt möchten wir Ihnen zeigen, wie Sie mit Core Animation eigene Eigenschaften eines Views animieren. Hierbei bedienen wir uns des Beispieles »Stackspector«, welches Sie bereits aus dem letzten Kapitel kennen.
HILFE Sie können als Ausgangspunkt das Projekt als Projekt »Stackspector Animation 1« von der Webseite herunterladen. Bei einem Klick auf einen Titelbereich soll die entsprechende Schraube langsam gedreht werden und die Fläche darunter langsam zuklappen.. Um dies zu erreichen, sind nur einige kleine Änderungen am Code notwendig. Bevor Sie allerdings Änderungen am Code vornehmen, sollten Sie das QuartzCore-Framework einbinden. Der Klasse ScrewDicslocure fügen wir eine Eigenschaft namens floatingState hinzu. Wir legen den Wertebereich dieser Eigenschaft auf das Interval 0-1 fest. Der Wert 0 steht hierbei für einen aufgeklappten Teilbereich und 1 steht für einen zugeklappten Teilbereich: @interface ScrewDisclosure : NSControl { } … @property CGFloat floatingState; @end
Wie bereits bei Cells besprochen, soll der Control nur als Front-End dienen und die Eigenschaft an die Cell weiterleiten. Entsprechende Implementierung: @implementation ScrewDisclosure @synthesize disclosedHeight;
479
SmartBooks
Objective-C und Cocoa – Band 2
- (CGFloat)floatingState { return [(ScrewCell*)[self cell] floatingState]; } - (void)setFloatingState:(CGFloat)value { [(ScrewCell *)[self cell] setFloatingState:value]; [self setNeedsDisplay:YES]; }
GRUNDLAGEN Wieso benötigen wir eine zusätzliche Eigenschaft? Würde es nicht reichen, die Frameänderung des Views zu animieren? Nein, weil dann zwei Probleme auftauchen: Zum einen hätten wir die Aufgabe aus der Überschrift verfehlt. Das mag hinnehmbar sein. Vor allem aber funktioniert das nicht, weil dann zwar der Frame animiert wäre, nicht jedoch die Drehung der Schraube mit ihrem Schatten. Wir benötigen daher eine zusätzliche Eigenschaft, die den Zustand des Schließvorganges genauer beschreibt. Entsprechendes machen wir für die Klasse ScrewCell: #import @interface ScrewCell : NSCell { CGFloat floatingState; } @property CGFloat floatingState; @end
Überlegen wir uns, was hier geschehen muss: Bisher hat -setState (ScrewCell) dafür gesorgt, dass sich der Disclosureview schließt und dieser wiederum teilte seinem Stackview mit, dass er die Subviews neu anordnun muss. Dieser Vorgang muss jetzt schrittweise vorgenommen werden, wenn sich der Floating-State verändert. Man kann allerdings dann nicht mehr einfach von -showContent und -hideContent sprechen. Wir benötigen vielmehr eine neue Methode -frameChanged, die wir gleich programmieren. Zunächst einmal muss jedoch die Änderung nicht nur bei -setState: (ScrewCell)sondern bei -setFloatingState: vorgenommen werden:
480
Kapitel 5
Core Image und Core Animation
… #import "ScrewDisclosure.h" … - (void)setState:(NSInteger)value { if( value != self.state ) { if( self.controlView != nil ) { [[self.controlView animator] setFloatingState:value]; } else { self.floatingState = value; } } [super setState:value]; } - (CGFloat)floatingState { return floatingState; } - (void)setFloatingState:(CGFloat)value { floatingState = value; if( self.controlView != nil ) { [((ScrewDisclosure *)self.controlView) frameChanged]; } }
Öffnen Sie bitte die Datei ScrewDisclosureCellSupport.m und passen Sie den Code wie folgt an, um die Animation zu aktivieren: @implementation ScrewDisclosure( ScrewDisclosureCellSupport ) - (void)frameChanged { NSSize size = self.frame.size; CGFloat minHeight = [self.cell cellSize].height;
481
SmartBooks
Objective-C und Cocoa – Band 2
CGFloat maxHeight = self.disclosedHeight; CGFloat height = (maxHeight - minHeight) * self.floatingState; size.height = minHeight + height; [self setFrameSize:size]; if( [self.superview isKindOfClass:[StackView class]] ) { StackView* stackView = (StackView*)self.superview; [stackView rearrangeForSubview:self]; } }
Die neue Methode geben wir dann im Header ScrewDisclosureCellSupport.h bekannt: @interface ScrewDisclosure( ScrewDisclosureCellSupport ) - (void)frameChanged;
Jetzt haben wir die Animationsfähigkeit strukturell implementiert. Wir sollten Sie noch bekannt geben, was am besten in -initWithFrame: von ScrewDisclosure geschieht: #import @implementation ScrewDisclosure … - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if( self ) … CABasicAnimation* animation = [CABasicAnimation animation]; NSDictionary* anims = [NSDictionary dictionaryWithObject:animation forKey:@"floatingState"]; [self setAnimations:anims]; } return self; } … @end
482
Kapitel 5
Core Image und Core Animation
Der eigentliche Trick liegt also in der Methode -initWithFrame:, in der eine CABasicAnimation erzeugt und mit dem Schlüssel floatingState in Verbindung gebracht wird. Falls nach dem Aufruf von -setAnimations: mit Hilfe eines Animators der Wert von floatingState geändert wird, so wird die entsprechende Animation gestartet. Im letzten Schritt müssen wir noch die Methode -drawInteriorWith Frame:inView: der Klasse ScrewCell anpassen. Im Prinzip müssen die Ifs und der dazugehörige Else-Zweig für den Status herausgenommen werden, da dieser ja jetzt nicht mehr nur noch ein boolscher Zustand, sondern ein Verlauf ist. Dafür müssen Zwischenwerte produziert werden: - (void)drawInteriorWithFrame:(NSRect)frame inView:(NSView*) control { … // Schraube zeichnen CGFloat progress = 1-self.floatingState; // Kreis [[NSGraphicsContext currentContext] saveGraphicsState]; NSShadow* shadow = [[[NSShadow alloc] init] autorelease]; NSSize offset; offset.width = +NSWidth( screwRect ) / 10.0 * progress; offset.height = -NSHeight( screwRect ) / 10.0 * progress; [shadow setShadowOffset:offset]; [shadow setShadowBlurRadius:4.0 * progress]; [shadow setShadowColor:[NSColor blackColor]]; [shadow set]; screwRect = NSInsetRect( screwRect, 4.0 - progress, 4.0 - progress ); … // …
Schlitz NSAffineTransform* transform = [NSAffineTransform transform]; [transform translateXBy:+NSMidX( screwRect ) yBy:+NSMidY( screwRect )]; [transform rotateByDegrees:90.0 * progress]; [transform translateXBy:-NSMidX( screwRect ) yBy:-NSMidY( screwRect )]; [line transformUsingAffineTransform:transform];
… }
483
SmartBooks
Objective-C und Cocoa – Band 2
Mittels der Multiplikation von self.floatingState mit 90 erreichen wir, dass der Wert 1 von self.floatingState einer Drehung um 90 Grad entspricht. Genau das, was wir erreichen wollen. Kompilieren und starten Sie das Programm und klicken Sie auf die Schrauben. Die Schrauben sollten sich in einer schönen Animation langsam rein- und herausdrehen lassen. Allerdings bemerken Sie auch sicherlich, dass unsere vereinfachung, auf einen eigenen Subview für den aufklappbaren Bereich zu verzichten, nicht sehr schön aussieht. hren Verständnis sollte das keinen Abbruch tun.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Stackspector Animation2« von der Webseite herunterladen.
Den Hintergrundfilter animieren Jetzt ist es an der Zeit, dass wir Ihnen ein etwas komplexeres Beispiel zeigen. Dieses Beispiel verdeutlicht, wie die Hintergrundfilter eines Views definiert und animiert werden. Zusätzlich zeigen wir Ihnen einen kleinen Trick mit Schlüsselpfaden im Zusammenhang mit Core Animation. Die Ausgangslage wird die folgende sein:
Die Kachelung wird durch einen Hintergrundfilter verzerrt.
Schauen Sie genau hin: Im zweiten Bild ist der Hintergrund (die weiß-graue Kachelung) ein wenig verzerrt. Der Grad der Zerrung des Hintergrundes ist nicht konstant. Wir lassen ihn von Core Animation animieren. Legen Sie ein neues Cocoaprojekt an und linken Sie gegen das QuartzCore-Framework. Erzeugen Sie einen View-Subclass namens FilteredContentView. Öffnen Sie die MainMenu.xib 484
Kapitel 5
Core Image und Core Animation
Datei und ändern Sie dort den Klassennamen des Content-Views des Fensters auf FilteredContentView. Ziehen Sie einige beliebige Controls, zum Beispiel Buttons und Textfelder, auf das Content-View. Markieren Sie anschließend alle hinzugefügten Controls, wählen Sie im Menü des Interface Builders unter Layout den Menüpunkt Embed Objects In und klicken Sie abschließend auf Custom View. Ihr Interface sollte nun in etwa wie die folgende Abbildung aussehen.
Klasse: FilteredContentView
Setzen Sie die Klasse des Views auf »FilteredContentView«.
Konzentrieren wir uns nun auf den Quellcode von FilteredContentView. Wir überschreiben einfach nur die Methoden –awakeFromNib und –drawRect:. #import "FilteredContentView.h" #import @implementation FilteredContentView - (void)drawRect:(NSRect)rect { } - (void)awakeFromNib { } @end
In der -drawRect: Methode wird lediglich eine Art Schachbrett, bestehend aus weißen und grauen Quadraten, gezeichnet. Dadurch werden die Auswirkungen des Hintergrundfilters, den wir gleich erzeugen, deutlich sichtbar. 485
SmartBooks
Objective-C und Cocoa – Band 2
- (void)drawRect:(NSRect)rect { CGFloat squareSize = 10.0f; NSUInteger colCount = ceil(NSWidth(self.bounds) / squareSize); NSUInteger rowCount = ceil(NSHeight(self.bounds) / squareSize); NSUInteger boxNumber = 0; for(NSUInteger row = 0; row != rowCount; row++) { for(NSUInteger col = 0; col != colCount; col++) { NSColor *fillColor = nil; if((row % 2) == (col % 2)) { fillColor = [NSColor lightGrayColor]; } else { fillColor = [NSColor whiteColor]; } [fillColor setFill]; NSRect boxRect = NSMakeRect(col * squareSize, row * squareSize, squareSize, squareSize); NSRectFill(boxRect); boxNumber++; } } }
Richtig interessant wird es aber erst in der Methode –awakeFromNib. - (void)awakeFromNib { [self setWantsLayer:YES]; CIVector *center = [CIVector vectorWithX:NSMidX(self.bounds) Y:NSMidY(self.bounds)]; CIFilter *filter = [CIFilter filterWithName:@"CIBumpDistortion" keysAndValues:
486
Kapitel 5
Core Image und Core Animation
kCIInputCenterKey, center, kCIInputRadiusKey, [NSNumber numberWithFloat:200.0f], kCIInputScaleKey, [NSNumber numberWithFloat:0.0f], nil]; filter.name = @"distortion"; [self.subviews.lastObject setBackgroundFilters: [NSArray arrayWithObject:filter]]; NSString *keyPath = [NSString stringWithFormat: @"backgroundFilters.distortion.%@", kCIInputScaleKey]; CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:keyPath]; animation.fromValue = [NSNumber numberWithFloat:0.0f]; animation.toValue = [NSNumber numberWithFloat:0.5f]; animation.duration = 1.0; animation.repeatCount = FLT_MAX; animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; animation.autoreverses = YES; [[self.subviews.lastObject layer] addAnimation:animation forKey:@"distortion Animation"]; }
Wie Sie wissen, wird die Methode -awakeFromNib aufgerufen, sobald die Nib-Datei, die eine Instanz von FilteredContentView enthält, geladen wurde (Band I, S. 352). Innerhalb der Methode -awakeFromNib geschieht nun Folgendes: Zunächst teilen wir der Instanz von FilteredContentView mittels -setWantsLayer: mit, dass mit ihr etwas Tolles geschehen soll, was jeden staunen lassen wird, der die Instanz zu Gesicht bekommt. Im Hintergrund wird automatisch eine Instanz von CALayer erzeugt und mit -setLayer: der Instanz zugewiesen. Dies hat zur Folge, dass fast alles, was das View betrifft (das Animieren, Setzen von Filtern, Ändern gewisser Style-Eigenschaften, ...), mit Hilfe dieser CALayer-Instanz realisiert wird. Core Animation ist jetzt in der Lage, ein Abbild des Views zu erzeugen. Genau ab diesem Moment kümmert sich Core Animation darum, dass Animationen, das Setzen von Filtern, Änderungen von 487
SmartBooks
Objective-C und Cocoa – Band 2
gewissen Style-Eigenschaften und all das, was sonst noch das Aussehen des Views betrifft, so performant wie möglich geschieht, da intern Core Animation OpenGL nutzt. Auf den Punkt gebracht bedeutet ein Aufruf von -setWantsLayer: mit dem Argument YES nichts weiter, als dass von nun an der Inhalt des Views mehr oder weniger gecached und spezifische Core Animation-Effekte möglich sind. Es ist sehr wichtig zu wissen, dass das Setzen von -wantsLayer auf YES zur Folge hat, dass die Eigenschaft -wantsLayer eines jeden Subviews auch auf YES gesetzt wird. Da im Beispiel FilteredContentView das Contentview eines Fensters ist, bedeutet das Setzen der Eigenschaft -wantsLayer auf YES, dass alle Views in diesem Fenster zu Layer-backed-Views werden. In der nächsten Zeile wird der Mittelpunkt des Views (in seinem eigenen Koordinatensystem) ermittelt und damit eine Instanz von CIVector erzeugt. Die Klasse CIVector gehört wie CIFilter zum Core Image Framework, welches Teil des Quartz Core Frameworks ist. CIVector repräsentiert Vektoren, deren Dimension zwischen eins und vier liegt. Anschließend wird mit +filterWithName:keysAndValues: eine Instanz von CIFilter erzeugt. Instanzen von CIFilter repräsentieren Core-Image-Filter, die Sie vielleicht von Photo Booth oder der Anwendung Core Image Fun House kennen. Konkret wird ein CIBumpDistortion-Filter erzeugt, der Wölbungen erzeugt. Nach der Erzeugung des Filters wird ihm ein Name verpasst. Der Name ist lediglich im Zusammenhang mit Core Animation wichtig. Der Wert ist beliebig und dennoch sehr wichtig. Dazu später mehr. Übrigens ist die name-Eigenschaft nicht in der Dokumentation zu CAFilter zu finden, da CAFilter keine name-Eigenschaft besitzt. CAFilter wird durch die Kategorie CAFilterInfo unter anderem durch die name-Eigenschaft ergänzt. Nun wird mit self.subviews.lastObject das letzte Subview ermittelt. Da die von Ihnen erzeugte Instanz von FilteredContentView lediglich einen direkten Subview enthält, zeigt self.subviews.lastObject auf das Custom View, welches alle weiteren Controls beinhaltet. Der erzeugte CIBumpDistortion-Filter wird mit -setBackgroundFilters: zum Hintergrundfilter des Custom-Views. Dies bedeutet, dass auf den Inhalt, also das, was mit -drawRect: gemalt wird, der CIBumpDistortion-Filter angewandt wird. Die nächsten Zeilen bewirken, dass die Ausprägung des Filters zwischen 0 und 0.5 animiert wird. Mittels +stringWithFormat: wird ein Schlüsselpfad konstruiert, mit dem die »InputScale«-Eigenschaft (kCIInputScaleKey) angesprochen werden kann. Wie Sie sehen, ist der Name des CIBumpDistortion-Filters (distortion) Teil des Schlüsselpfades. Nun sollte Ihnen klar werden, wieso der erzeugte Filter einen Namen erhalten musste. Dies ermöglicht den unkomplizierten Zugriff auf den Filter selbst beziehungsweise auf dessen Eigenschaften. Mit -animationWithKeyPath: wird eine Instanz von CABasicAnimation erzeugt, welche sich auf den konstruierten Schlüsselpfad bezieht. Die Animation animiert den Wert, der sich hinter dem Schlüsselpfad versteckt, innerhalb von einer Sekunde von 0.0 nach 0.5. Die Animation wird unendlich oft durchgeführt. Erreicht wird dies, indem die Eigenschaft repeatCount auf eine sehr hohe Zahl (FLT_MAX) gesetzt wird. Am Ende wird der Layer des einzigen Subviews ermittelt und mittels -addAnimation:forKey: über die erzeugte Animation informiert. Wichtig ist, dass die Animation der Wölbung des 488
Kapitel 5
Core Image und Core Animation
Hintergrundfilters kein Neuzeichnen zur Folge hat. Anhand dieses Beispiels haben Sie gelernt, welche Auswirkungen der Aufruf -setWantsLayer: hat, wie Sie Filter erzeugen, diese benennen und benutzen. Außerdem wissen Sie nun, dass Sie benannte Filter ganz simpel mittels Schlüsselpfaden ansprechen und animieren können.
Alles zusammen: Core Image und Core Animation Core Animation ist recht gut in das AppKit-Framework integriert. Werfen Sie nochmals einen Blick auf das ColorControls-Beispiel des Kapitels zu Core Image. In diesem Beispiel wurde mit Hilfe des CIColorControls-Filters Sättigung, Kontrast und Helligkeit eines Bildes manipuliert. Es mussten allerdings einige Arbeiten geleistet werden, die nicht nötig waren. Zum Beispiel musste eine Instanz von CIImage erstellt werden. Außerdem musste für den CIColorControls-Filter eine Eigenschaft (filter) verwaltet, bei jeder Änderung des Wertes eines Sliders das Ausgabebild ermittelt, in ein NSImage umgewandelt und im Imageview angezeigt werden. Wäre es nicht schön, wenn jedes View seine eigenen Filter verwalten würde? Genau das ist seit OS X 10.5 möglich. Jede Instanz von NSView verfügt über Methoden, mit denen dessen Filter festgelegt werden können. Diese Methoden heißen -setBackgroundFilters:, -setCompositingFilter: und -setContentFilters:. Da jedes View über diese Methoden verfügt, verfügt auch ein Imageview über diese Methoden. Die unnötigen Arbeiten entfallen bei der Nutzung dieser Methoden komplett. Möglich wurden diese Methoden durch Core Animation. Lassen Sie uns dennoch einen genaueren Blick auf eine der Methoden werfen, die einen Filter eines Views setzen. Interessant für das Beispiel sind die Content-Filter eines Views. Mit ihnen ist es möglich, das erste Beispiel zu optimieren. Die Eigenschaft filter fällt komplett weg, da stattdessen die Eigenschaft contentFilters des Imageviews genutzt werden kann. Die Methoden -updateOutputImageView und -init fallen ebenfalls komplett weg. Allerdings ändert sich die Implementierung der Methode -awakeFromNib. @implementation ColorControlsAppDelegate - (void)awakeFromNib { [outputImageView setWantsLayer:YES]; [outputImageView setImage:[self originalImage]]; CIFilter *filter = [CIFilter filterWithName:@"CIColorControls"]; [filter setDefaults]; NSArray *newFilters = [NSArray arrayWithObject:filter]; [outputImageView setContentFilters:newFilters]; } …
489
SmartBooks
Objective-C und Cocoa – Band 2
Der Aufruf -setWantsLayer: aktiviert Core Animation (Näheres dazu im Abschnitt zu Core Animation). Anschließend wird wie gewohnt der CIColorControls-Filter erzeugt. Allerdings wird kein Eingabebild definiert. Dies ist auch nicht nötig, da der Color-Controls-Filter als Content-Filter fungieren wird. Die Content-Filter werden automatisch der Reihe nach auf den Inhalt des Imageviews angewandt. Die Actionmethoden -takeSaturationFromSender:, -takeBrightnessFromSender: und -takeContrastFromSender: müssen leicht abgewandelt werden. … #pragma mark Actions - (IBAction)takeSaturationFromSender:(id)sender { float saturation = [sender floatValue]; NSNumber* newValue = [NSNumber numberWithFloat:saturation]; CIFilter* filter = [[[outputImageView contentFilters] lastObject] copy]; [filter setValue:newValue forKey:kCIInputSaturationKey]; NSArray *newtFilters = [NSArray arrayWithObject:filter]; [outputImageView setContentFilters:newtFilters]; [filter release]; } - (IBAction)takeBrightnessFromSender:(id)sender { float brightness = [sender floatValue]; NSNumber* newValue = [NSNumber numberWithFloat:brightness]; CIFilter* filter = [[[outputImageView contentFilters] lastObject] copy]; [filter setValue:newValue forKey:kCIInputBrightnessKey]; NSArray *newtFilters = [NSArray arrayWithObject:filter]; [outputImageView setContentFilters:newtFilters]; [filter release]; } - (IBAction)takeContrastFromSender:(id)sender { float contrast = [sender floatValue]; NSNumber* newValue = [NSNumber numberWithFloat:contrast]; CIFilter* filter = [[[outputImageView contentFilters] lastObject] copy]; [filter setValue:newValue forKey:kCIInputContrastKey]; NSArray *newtFilters = [NSArray arrayWithObject:filter];
490
Kapitel 5
Core Image und Core Animation
[outputImageView setContentFilters:newtFilters]; [filter release]; }
In jeder dieser Methoden geschieht im Prinzip das Gleiche. Der aktuelle »ContentFilter« wird ermittelt. Anschließend wird der Filter mit dem neuen Wert versorgt und in ein Array gehüllt, welches dann mittels -setContentFilters: dem ImageView übergeben wird. Natürlich werden die »Content-Filters« auf den gesamten Inhalt des ImageViews angewandt, also auch auf den Rahmen des ImageViews. Um dieses Problem zu beheben, können Sie dem ImageView mitteilen, dass es keinen Rahmen anzeigen soll. Möchten Sie nicht auf einen Rahmen für das ImageView verzichten, so können Sie dieses einfach zu einem Subview einer Box machen.
HILFE Sie können das modifizierte Projekt in diesem Zustand als »ColorControlsWithCoreAnimation« von der Webseite herunterladen.
Mehr über Layer im Zusammenspiel mit Views Sie wissen bereits, dass ein Layer alleine nie auf dem Bildschirm angezeigt werden kann. Um einen Layer anzeigen zu können, muss er einem View übergeben werden, welches den Layer dann anzeigen kann. Konkret kann ein Layer einem View wie folgt übergeben werden: #import … - (void)awakeFromNib { [view setWantsLayer:YES]; [view setLayer:[CALayer layer]]; [[view layer] setBackgroundColor: CGColorCreateGenericRGB(1.0f, 0.0f, 0.0f, 0.5f)]; }
Mit der Klassenmethode +layer der Klasse CALayer wird ein Layer erzeugt und mit -setLayer: dem View übergeben. Anschließend wird dem View noch mitgeteilt, dass es den Layer benutzen soll. Zuletzt wird der Hintergrund des Layers mit -setBackgroundColor: noch rot eingefärbt. Lassen Sie sich von der Funktion CGColorCreateGenericRGB nicht irritieren. -setBackgroundColor: von CALayer erwartet als Argument ein »Objekt« der Struktur CGColorRef und nicht, wie Sie vielleicht annehmen, 491
SmartBooks
Objective-C und Cocoa – Band 2
ein Objekt vom Typ NSColor. Die Layerklassen bevorzugen an vielen Stellen C-Strukturen. Es kann angenommen werden, dass Apple mit dem Verzicht auf NSColor keine Abhängigkeiten zum AppKit-Framework entstehen lassen wollte. Das iPhone SDK kennt nämlich kein AppKit-Framework, wohl aber Core Animation. Probieren Sie das Beispiel aus. Den Code können Sie 1:1 übernehmen. Die Variable view ist ein Outlet vom Typ NSView. Sie sollten folgendes Ergebnis zu Gesicht bekommen:
Der Root-Layer eines Views nimmt immer das gesamte View ein.
Sofort fällt auf, dass standardmäßig der Layer seine Größe dem des Views anpasst. Dies hat einen guten Grund: Ein View ist lediglich in der Lage, einen einzigen Layer zu verwalten. Daher passt sich der Layer immer der Größe des Views an, um keinen Platz zu verschwenden. Aber keine Angst, ein Layer kann über mehrere Sublayer verfügen, und jeder Sublayer kann wiederum Sublayer haben. Sie können folglich mit Layern eine baumartige Hierarchie erstellen, mit der komplexe Daten visualisiert werden können.
Der Inhalt eines Layers Wie Sie die Style-Eigenschaften eines Layers ändern, haben Sie im vorherigen Kapitel kennengelernt. Nun ist es an der Zeit, dass Sie etwas über den Inhalt eines Layers erfahren. Instanzen der Klasse CALayer verfügen über eine Eigenschaft namens contents. Obwohl diese Eigenschaft vom Typ id ist, erwartet sie einen Zeiger 492
Kapitel 5
Core Image und Core Animation
auf ein CGImage (CGImageRef). CGImage repräsentiert ein Bild. Der Wert von contents könnte wie folgt definiert werden: #import "AppController.h" #import @implementation AppController - (void)awakeFromNib { CALayer *rootLayer = [CALayer layer]; NSData *imageData = [[NSImage imageNamed:NSImageNameDotMac] TIFFRepresentation]; NSBitmapImageRep *irep = [[[NSBitmapImageRep alloc] initWithData:imageData] autorelease]; [rootLayer setContents:(id)[irep CGImage]]; [view setLayer:rootLayer]; [view setWantsLayer:YES]; } @end
HILFE Sie können das Projekt in diesem Zustand als Projekt »ImageContents« von der Webseite herunterladen. Zunächst wird wieder mit +layer ein Layer erzeugt. Mit +imageNamed: wird eine Instanz von NSImage erzeugt. Die Methode -TIFFRepresentation liefert die Rohdaten des erzeugten Bildes. Mit diesen Rohdaten kann dann mittels -initWithData: eine Instanz von NSBitmapImageRep erzeugt werden. Seit OS X 10.5 verfügt die Klasse NSBitmapImageRep über die Methode -CGImage, welche einen Zeiger auf ein CGImage liefert, das mit den entsprechenden Rohdaten erzeugt wurde. Der Zeiger wird auf id gecasted und mit -setContents: dem Layer übergeben. Es ist folglich nicht notwendig, eine Subklasse von CALayer zu erzeugen, um dort das Bild zu zeichnen. Der Inhalt eines Layers kann jederzeit geändert werden. Die Eigenschaft contents ist sogar animierbar. Der Inhalt eines Layers ist oft recht einfach und mit einem einzigen Bild darstellbar. Möchten Sie in einem Layer allerdings komplexere Dinge darstellen und vielleicht vorhandene Zeichenroutinen nutzen, so kommt für Sie eher die zweite Möglichkeit in Frage, mit der Sie einen Layer mit Inhalt füllen können. Jedem Layer können Sie mit -setDelegate: ein Delegate übergeben. CALayer verfügt aktuell über drei Delegatemethoden. Eine davon ist -drawLayer:inContext: , mit der dem Delegate die Möglichkeit gegeben wird, den Inhalt 493
SmartBooks
Objective-C und Cocoa – Band 2
eines Layers selbst zu zeichnen. Es ist wieder erwähnenswert, dass der übergebene Kontext vom Typ CGContextRef und nicht etwa vom Typ NSGraphicsContext ist. Es stellt sich nun die Frage, ob Sie zwangsläufig in einen CGContextRef zeichnen müssen. Dies würde bedeuten, dass Sie Ihre vorhandenen Zeichenroutinen nicht nutzen könnten. Allerdings existiert die Möglichkeit, innerhalb der Methode einen NSGraphicsContext zu erzeugen und dann in diesen zu zeichnen. #import "AppController.h" #import @implementation AppController - (void)awakeFromNib { CALayer *rootLayer = [CALayer layer]; [view setLayer:rootLayer]; [view setWantsLayer:YES]; [[view layer] setDelegate:self]; [[view layer] setNeedsDisplay]; } - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx { NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithGraphicsPort:ctx flipped:NO]; [NSGraphicsContext saveGraphicsState]; [NSGraphicsContext setCurrentContext:context]; [[NSColor yellowColor] set]; NSRect rect = NSRectFromCGRect([layer bounds]); [[NSBezierPath bezierPathWithRect:rect] fill]; [NSGraphicsContext restoreGraphicsState]; } @end
HILFE Sie können das Projekt in diesem Zustand als Projekt »DelegateContents« von der Webseite herunterladen.
494
Kapitel 5
Core Image und Core Animation
Mit +graphicsContextWithGraphicsPort:flipped: wird ein gewöhnlicher Graphics context erzeugt. Anschließend wird der aktuelle Graphikstatus gespeichert und mit +setCurrentContext: der aktuelle Graphikkontext modifiziert. Nach dem entsprechenden Aufruf von +setCurrentContext: kann wie gewohnt mit den Klassen aus AppKit (NSBezierPath, NSGradient, ...) gezeichnet werden. Sollten Sie die Implementierung der Zeichenlogik in einer Subklasse von CALayer bevorzugen, so können Sie dies in der Methode -drawInContext: erledigen. Dabei können Sie, ähnlich wie im vorherigen Beispiel, einen Graphikkontext erzeugen und nutzen.
Der Nutzen von Layern Ganz frech stellen wir die Frage in den Raum, wieso es neben der Integration von Core Animation in die bestehende, auf Views basierende Architektur überhaupt Layerklassen gibt. Was ist so toll an den Layerklassen? Wie Sie bereits wissen, muss sich ein Layer, der angezeigt werden soll, in einem View befinden. Sie wissen auch, dass ein Aufruf von -setWantsLayer: mit YES als Argument automatisch das entsprechende View in den Core-Animation-Modus versetzt, eine Instanz von CALayer erzeugt und der layer-Eigenschaft des Views zuweist. Beobachten wir das Ganze ein wenig genauer: Erzeugen Sie ein neues Cocoaprojekt. Linken Sie gegen das QuartzCore-Framework. Widmen wir uns gleich der Implementierung des Delegates. #import "LayerErzeugungAppDelegate.h" @implementation LayerErzeugungAppDelegate @synthesize window; - (void)awakeFromNib { [super awakeFromNib]; NSView *view = self.window.contentView; NSLog(@"layer: %@", [view layer]); view.wantsLayer = YES; NSLog(@"layer: %@", [view layer]); } @end
495
SmartBooks
Objective-C und Cocoa – Band 2
Kurze Erklärung: Die Methode -awakeFromNib wird automatisch aufgerufen. In ihr wird zunächst die Beschreibung des Layers des Views ausgegeben. Anschließend wird dem View mit -setWantsLayer: mitgeteilt, dass Core Animation für dieses View aktiviert werden soll. Zum Schluss wird nochmals die Beschreibung des Layers des Views ausgegeben. Bevor Sie das Beispiel abtippen, kompilieren und ausführen, wäre es sinnvoll zu überlegen, welches Resultat erwartet wird. Standardmäßig hat ein View kein Layer. -layer liefert also nil. Der Aufruf von -setWantsLayer: mit YES als Argument erzeugt allerdings einen Layer. Also ist -layer nach diesem Aufruf nicht nil. Die Ausgabe auf der Konsole dürfte in etwa wie folgt aussehen: >… layer: (null) >… layer: _NSViewBackingLayer(0x13da60) p={0, 0} b=(0,0,440,320) superlayer=0x0
Der entsprechende Aufruf von -setWantsLayer: erzeugt also nicht direkt einen CALayer, sondern eine Instanz der privaten Subklasse _NSViewBackingLayer von CALayer. Sie können allerdings das View mit einer eigenen Instanz von CALayer versorgen. Beachten Sie hierbei die strikte Teilung der Aufgaben. Das View, welches einen Layer beherbergt, übernimmt das gesamte Handling von Events, die das View betreffen. Eine Instanz von CALayer kennt keine Events. Aber wieso sollten Sie ein View mit einem eigenen Layer versehen? Es gibt einige Szenarios, in denen so etwas sinnvoll ist. Nehmen wir an, Sie möchten ein View implementieren, welches ein oder mehrere Fotos zeigt. Bei einem Klick auf eins der angezeigten Fotos soll sich das Foto um 180 Grad drehen und seine Rückseite anzeigen, auf der einige Daten (Größe, Titel, ...) des Fotos stehen. Was bisher nämlich noch keine Erwähnung fand, ist die Tatsache, dass jeder Layer unter anderem ein Rechteck, also eine zweidimensionale Fläche, in einem dreidimensionalen Raum beschreibt. Jede Instanz von CALayer verfügt über eine Eigenschaft namens transform, die eine C-Struktur des Typs CATransform3D liefert. Der Name der Struktur verdeutlicht, dass Strukturen des Typs CATransform3D Transformationen im dreidimensionalen Raum beschreiben. Mittels -setTransform: können Sie jeden Layer im dreidimensionalen Raum drehen, skalieren und verschieben. Ein Layer kann beispielsweise wie folgt um 180 Grad gedreht werden. [layer setTransform:CATransform3DRotate([layer transform], M_PI * 0.5f, 1.0f, 0.5f, 1.0f)];
Die Eigenschaft -transform ist übrigens auch animierbar.
496
Kapitel 5
Core Image und Core Animation
Mehrere Layer in einem View Die Verwendung von Layern ist erst dann richtig sinnvoll, wenn mehrere Layer in einem View ihre Arbeit tun. Allerdings wird lediglich ein einziger Layer von einem View direkt verwaltet. Jedoch kann ein Layer über Sublayer verfügen. Lassen Sie uns jetzt an einem kleinen Beispiel verstehen, wie zu einem Layer Sublayer hinzugefügt werden können. Interessant ist auch die Frage, wie die Sublayer in ihrem Superlayer positioniert werden. Ziel wird es sein, ein View mit einem Layer zu versorgen, der zwei Sublayer hat. Der eine Sublayer soll die linke Hälfte seines Superlayers einnehmen. Der andere Sublayer soll die rechte Hälfte seines Superlayers einnehmen. Wird das View, in dem sich die Layer befinden, in seiner Größe verändert, so sollen sich die Layer in ihrer Größe und Position entsprechend anpassen. Das Resultat wird in etwa wie folgt aussehen:
Zwei Layer in einem View. Das funktioniert nur mit zwei Sublayern des Root-Layers.
Legen Sie ein neues Projekt in Xcode an. Linken Sie gegen das QuartzCore-Frame work. Nennen Sie das Projekt ZweiLayer. Das Delegate der Anwendung wird ein View so modifizieren, dass es so aussieht wie in der obigen Abbildung. Dies bedeutet, dass die Klasse ZweiLayerAppDelegate ein Outlet auf ein View benötigt. #import @interface ZweiLayerAppDelegate : NSObject { NSView *view; } @property (assign) IBOutlet NSView *view; @end
497
SmartBooks
Objective-C und Cocoa – Band 2
Öffnen Sie nun mit dem Interface Builder die Datei »MainMenu.xib«. Ziehen Sie ein NSCustomView auf das Fenster. Passen Sie die Autosizing-Optionen so an, dass das View seine Größe dem des Fensters anpasst. Verbinden Sie anschließend das view-Outlet vom ZweiLayerAppDelegate mit dem eben erzeugten View. Wenden Sie sich nun der Implementierung von ZweiLayerAppDelegate zu. Wir werden lediglich die Methode –awakeFromNib überschreiben. #import "ZweiLayerAppDelegate.h" #import @implementation ZweiLayerAppDelegate - (void)awakeFromNib { } @end
ZweiLayerAppDelegate wird das View in der Methode -awakeFromNib verändern. Kurz zusammengefasst wird in -awakeFromNib ein Layer erzeugt, der zum Layer des Views wird. Die letzte Zeile wirft allerdings Fragen auf. Jeder Layer kann über einen Layoutmanager verfügen. Ein Layoutmanager ist für die Positionierung der Subviews des Layers verantwortlich. Der Layoutmanager eines Layers ist entweder eine Instanz von CAConstraintLayoutManager oder ein beliebiges Objekt, welches das Layout-Manager-Protokoll implementiert. Im Falle des Beispiels wird allerdings ein CAConstraintLayoutManager verwendet. Einerseits um zu zeigen, wie er funktioniert, und andererseits, da er uns die Implementierung des LayoutManager-Protokolls abnimmt. »Constraint« bedeutet so viel wie »Bedingung«. Wir müssen folglich die gewünschte Anordnung der Sublayers mit Bedingungen beschreiben. - (void)awakeFromNib { [view setWantsLayer:YES]; [view setLayer:[CALayer layer]]; CAConstraintLayoutManager *manager = [CAConstraintLayoutManager layoutManager]; [[view layer] setLayoutManager:manager]; }
Ein Layout-Manager alleine reicht aber noch nicht. Wir müssen ihm noch genau sagen, welche Layer er wie anordnen soll. Daher wird jetzt die Implementierung von –awakeFromNib erweitert. Zunächst wird eine Instanz, leftLayer, von CAText498
Kapitel 5
Core Image und Core Animation
Layer erzeugt. Mit -setString: wird festgelegt, dass leftLayer den Text linker Sublayer anzeigen soll. Jeder Layer kann einen Namen besitzen. Besitzt ein Layer einen Namen, so können sich verschiedene »Stellen« auf den Layer mit Hilfe seines Namens beziehen. Dann wird der Layer noch grün eingefärbt. Mittels –addSublayer: wird leftLayer zu einem Sublayer des Layers des Views. Nun versorgen wir leftLayer mit Bedingungen, die seine Positionierung innerhalb seines Superlayers festlegen. Wir beginnen damit, dass wir leftLayer mitteilen, nur halb so breit wie sein Superlayer zu sein, so dass noch Platz für einen zweiten Layer ist. Jede Instanz von CAConstraint beschreibt eine Bedingung. Im ersten Schritt wird mittels +constraintWithAttribute :relativeTo:attribute:scale:offset: eine Instanz von CAConstraint erzeugt. In Worten formuliert lässt sich die erzeugte Instanz von CAConstraint wie folgt beschreiben: »Die Instanz widthConstraint beschränkt die Breite (kCAConstraintWidth) ihres Layers relativ zu der Breite (kCAConstraintWidth) ihres Superlayers (@"superlayer") auf die Hälfte (scale 0.5f).« Das Argument relativeTo erwartet den Namen eines Layers. Der verwendete Name superlayer ist ein spezieller. Unabhängig davon, wie der Superlayer von leftLayer heißt, ist es möglich, sich mit superlayer auf seinen Superlayer zu beziehen. Neben dem Attribut Breite (kCAConstraintWidth) existieren noch Höhe (kCAConstraintHeight), x-Koordinate des Ursprungs (kCAConstraintMinX), y-Koordinate des Ursprungs (kCAConstraintMinY), die größte x-Koordinate (kCAConstraintMaxX), die größte y-Koordinate (kCAConstraint MaxY), die mittlere x-Koordinate (kCAConstraintMidX) und die mittlere y-Koordinate (kCAConstraintMidY). Die Beschränkung der Breite von leftLayer genügt allerdings noch nicht. Die Bedingung heightConstraint bewirkt, dass sich die Höhe von leftLayer der Höhe des Superlayers von leftLayer anpasst. leftLayer soll die linke Hälfte einnehmen. Mit diesen Zeilen wird die Position von leftLayer festgelegt. Nun muss noch der rechte Layer erzeugt werden. Dies funktioniert analog zur Erzeugung von leftLayer. Es wird ein weiterer CATextLayer erzeugt, blau eingefärbt und als Sublayer vom Layer des Views hinzugefügt. Nun muss auch die Positionierung von rightLayer mit Instanzen von CAConstraint beschrieben werden. Die Breite soll wie bei leftLayer auch auf die Hälfte der Breite des Superlayers beschränkt werden. rightLayer soll auch wie leftLayer die gesamte verfügbare Höhe einnehmen. Mit rightMinXConstraint wird der Ursprung der x-Koordinate von rightLayer auf die rechte untere Ecke von leftLayer gesetzt. Sie sehen, dass diese Bedingung relativ zu leftLayer formuliert wird. Daher war es wichtig, leftLayer einen Namen zu geben. - (void)awakeFromNib { [view setWantsLayer:YES]; [view setLayer:[CALayer layer]]; CAConstraintLayoutManager *manager = [CAConstraintLayoutManager layoutManager];
499
SmartBooks
Objective-C und Cocoa – Band 2
[[view layer] setLayoutManager:manager]; [self setFrontSideLayer:[CALayer layer]]; [[self frontSideLayer] setDoubleSided:NO]; [[self layer] addSublayer:[self frontSideLayer]]; [[self frontSideLayer] addConstraint: [CAConstraint constraintWithAttribute:kCAConstraintMidX relativeTo:@"superlayer" attribute:kCAConstraintMidX]]; [[self frontSideLayer] addConstraint: [CAConstraint constraintWithAttribute:kCAConstraintWidth relativeTo:@"superlayer" attribute:kCAConstraintWidth]]; [[self frontSideLayer] addConstraint: [CAConstraint constraintWithAttribute:kCAConstraintMaxY relativeTo:@"superlayer" attribute:kCAConstraintMaxY]]; [[self frontSideLayer] addConstraint: [CAConstraint constraintWithAttribute:kCAConstraintHeight relativeTo:@"superlayer" attribute:kCAConstraintHeight]]; NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"image" ofType:@"JPG"]]; CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)url, NULL); CGImageRef imageRef = NULL; if (source) { imageRef = CGImageSourceCreateImageAtIndex(source, 0, NULL); [[self frontSideLayer] setContents:(id)imageRef]; CFRelease(source); } [self setBackSideLayer:[CATextLayer layer]]; [[self backSideLayer] setString: @"Kirchplatz\nPhilippsburg\n\nJuni 2005"];
500
Kapitel 5
Core Image und Core Animation
[[self backSideLayer] setBackgroundColor: CGColorCreateGenericRGB(0.0f, 0.0f, 0.0f, 0.8f)]; [[self backSideLayer] setDoubleSided:NO]; [[self layer] addSublayer:self.backSideLayer]; NSArray *constraints = [[self frontSideLayer] constraints]; for(CAConstraint *constraint in constraints) { [[self backSideLayer] addConstraint:constraint]; } [[self backSideLayer] setTransform: CATransform3DMakeRotation(M_PI, 0.0f, 1.0f, 0.0f)]; }
HILFE Sie können das Projekt in diesem Zustand als »ZweiLayer« von der Webseite herunterladen. Abschließend wird die minYConstraint wieder verwendet, um die y-Koordinate des Ursprunges von rightLayer festzulegen. Kompilieren und starten Sie das Beispiel. Die Größe und Position von leftLayer und rightLayer passen sich automatisch entsprechend an. Es ist kein weiterer Code notwendig. Mit CAConstraint und CAConstraintLayoutManager können neben einfachen Layouts auch komplexe Layouts beschrieben werden. Es ist zum Beispiel möglich, nur mit CAConstraintLayoutManager und CAConstraint Layer schachbrettartig anzuordnen. Machen Sie sich bitte nochmals klar, was wir hier aufgebaut haben. Die folgende Graphik hilft Ihnen dabei. Linker Sublayer
Root Layer
Rechter Sublayer
View
Im View liegt der Root Layer, und darin liegen zwei Sublayer.
501
SmartBooks
Objective-C und Cocoa – Band 2
Transformationen in 3D Das letzte Beispiel zu Core Animation soll verdeutlichen, wie Transformationen auf Layer im dreidimensionalem Raum angewandt werden. Sie werden ein View erstellen, welches ein Foto zeigt. Klickt der Benutzer auf das Foto, so dreht sich das Foto um 180 Grad und zeigt seine Rückseite, auf der einige Informationen zu dem Foto angezeigt werden.
Das Foto wird gedreht und zeigt nach einiger Zeit seine Rückseite an.
Wie das prinzipiell funktioniert, ist eigentlich recht einfach: Benötigt wird ein View mit einem Layer. Der Layer des Views enthält zwei Sublayer. Den frontSideLayer, der das Foto enthält, und den backSideLayer, der die Informationen enthält. Der backSideLayer sollte anfangs nicht sichtbar sein. Dies wird erreicht, indem der backSideLayer einfach um 180 Grad gedreht wird. Wäre dann nicht die Rückseite von backSideLayer sichtbar? Richtig! Wir teilen Core Animation aber mit, dass backSideLayer keine Rückseite haben soll. Wird ein Layer, der keine Rückseite hat, um 180 Grad gedreht, so verschwindet er einfach. Klickt der Benutzer auf das View, so werden der frontSideLayer und der backSideLayer einfach um 180 Grad gedreht. Der frontSideLayer hat natürlich wie backSideLayer keine Rückseite. Lassen Sie uns nun mit der Arbeit beginnen. Legen Sie mit Xcode ein neues Projekt an. Linken Sie wie immer gegen das QuartzCore-Framework. Fügen Sie Ihrem Projekt ein NSView-Subclass namens DoubleSidedPhotoView hinzu. Öffnen Sie im Inter502
Kapitel 5
Core Image und Core Animation
face Builder die Datei MainMenu.xib. Ziehen Sie eine Instanz von NSCustomView auf das Fenster und ändern Sie dessen Klassenname auf DoubleSidedPhotoView. DoubleSidedPhotoView wird den frontSideLayer sowie den backSideLayer oft direkt manipulieren. Daher hält DoubleSidedPhotoView eine Referenz auf frontSideLayer sowie eine Referenz auf backSideLayer. #import @class CALayer, CATextLayer; @interface DoubleSidedPhotoView : NSView { CALayer *frontSideLayer; CATextLayer *backSideLayer; } @property (retain) CALayer *frontSideLayer; @property (retain) CATextLayer *backSideLayer; @end
Der backSideLayer soll Text anzeigen. Daher ist backSideLayer ein CATextlayer. Wenden wir uns nun der Implementierung von DoubleSidedPhotoView zu: Das Grundgerüst macht deutlich, dass lediglich die Methoden –awakeFromNib und –mouseDown: überschrieben werden. #import "DoubleSidedPhotoView.h" #import @implementation DoubleSidedPhotoView @synthesize frontSideLayer, backSideLayer; - (void)awakeFromNib { } - (void)mouseDown:(NSEvent *)event { } @end
Wie so oft kann viel Arbeit direkt in der Methode -awakeFromNib erledigt werden. Es wird wieder Core Animation aktiviert, das View mit einem Layer versorgt und ein CAConstraintLayoutManager verwendet. Dann wird der frontSideLayer erzeugt. Der Aufruf -setDoubleSided: mit dem Argument NO bewirkt, dass der frontSideLayer keine Rückseite hat und bei einer Drehung um 180 Grad un503
SmartBooks
Objective-C und Cocoa – Band 2
sichtbar wird. Wird frontSideLayer nach einer Drehung um 180 Grad wieder um 180 Grad gedreht, wird er wieder sichtbar. Anschließend werden die Bedingungen von frontSideLayer in der Art formuliert, dass er die gesamte Größe seines Superlayers einnimmt.Der frontSideLayer soll ein Bild beziehungsweise ein Foto anzeigen. Ziehen Sie in Xcode ein beliebiges Bild in den Ordner Resources und nennen Sie es » image.JPG«. Dieses Bild soll von frontSideLayer angezeigt werden. Mit -pathForResource:ofType: wird der Pfad des Bildes ermittelt. Anschließend wird ein CGImage erzeugt und dem frontSideLayer mittels -setContents: übergeben. Der frontSideLayer ist fertig. Nun wird der backSideLayer erzeugt. Der backSideLayer besitzt wie frontSideLayer keine Rückseite. frontSideLayer passt seine Größe der Größe seines Superlayers an. Für den backSideLayer soll das Gleiche gelten. Der backSideLayer erhält folglich die Bedingungen, über die auch der frontSideLayer verfügt. Der Layer des Views verfügt jetzt über zwei Sublayer, die beide keine Rückseite besitzen und den gesamten Platz, den der Layer zu bieten hat, einnehmen. - (void)awakeFromNib { [self setWantsLayer:YES]; [self setLayer:[CALayer layer]]; [[self layer] setLayoutManager: [CAConstraintLayoutManager layoutManager]]; [self setFrontSideLayer:[CALayer layer]]; [[self frontSideLayer] setDoubleSided:NO]; [[self layer] addSublayer:[self frontSideLayer]]; [[self frontSideLayer] addConstraint: [CAConstraint constraintWithAttribute:kCAConstraintMidX relativeTo:@"superlayer" attribute:kCAConstraintMidX]]; [[self frontSideLayer] addConstraint: [CAConstraint constraintWithAttribute:kCAConstraintWidth relativeTo:@"superlayer" attribute:kCAConstraintWidth]]; [[self frontSideLayer] addConstraint: [CAConstraint constraintWithAttribute:kCAConstraintMaxY relativeTo:@"superlayer" attribute:kCAConstraintMaxY]];
504
Kapitel 5
Core Image und Core Animation
[[self frontSideLayer] addConstraint: [CAConstraint constraintWithAttribute:kCAConstraintHeight relativeTo:@"superlayer" attribute:kCAConstraintHeight]]; NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"image" ofType:@"JPG"]]; CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)url, NULL); CGImageRef imageRef = NULL; if( source ) { imageRef = CGImageSourceCreateImageAtIndex(source, 0, NULL); [[self frontSideLayer] setContents:(id)imageRef]; CFRelease(source); } [self setBackSideLayer:[CATextLayer layer]]; [[self backSideLayer] setString: @"Kirchplatz\nPhilippsburg\n\nJuni 2005"]; [[self backSideLayer] setBackgroundColor: CGColorCreateGenericRGB(0.0f, 0.0f, 0.0f, 0.8f)]; [[self backSideLayer] setDoubleSided:NO]; [[self layer] addSublayer:self.backSideLayer]; NSArray *constraints = [[self frontSideLayer] constraints]; for(CAConstraint *constraint in constraints) { [[self backSideLayer] addConstraint:constraint]; } [[self backSideLayer] setTransform: CATransform3DMakeRotation(M_PI, 0.0f, 1.0f, 0.0f)]; }
Jetzt ist es an der Zeit, dass der backSideLayer verschwindet. Da er keine Rückseite besitzt, muss er lediglich um 180 Grad gedreht werden. Weg ist er. Bei einem Klick auf das View soll die Rückseite des Bildes (also der backSideLayer) angezeigt werden. Dies könnte erreicht werden, indem der frontSideLayer sowie der backSide505
SmartBooks
Objective-C und Cocoa – Band 2
Layer um 180 Grad gedreht werden. Die Methode -mouseDown: wird automatisch aufgerufen, sobald der Benutzer auf das View geklickt hat. - (void)mouseDown:(NSEvent *)event { [NSAnimationContext beginGrouping]; [[NSAnimationContext currentContext] setDuration:5.0f]; CATransform3D frontLayerTransform = CATransform3DRotate( [[self frontSideLayer] transform], M_PI, 0.0f, 1.0f, 0.0f); [[self frontSideLayer] setTransform:frontLayerTransform]; CATransform3D backLayerTransform = CATransform3DRotate( [[self backSideLayer] transform], M_PI, 0.0f, 1.0f, 0.0f); [[self backSideLayer] setTransform:backLayerTransform]; [NSAnimationContext endGrouping]; }
HILFE Sie können das Projekt in diesem Zustand als Projekt »RotateLayer« von der Webseite herunterladen. So richtig neu ist hier die Klasse NSAnimationContext. Diese ist durchaus wichtig. Sie funktioniert ähnlich wie NSGraphicsContext. Mit NSAnimationContext können Sie mittels +beginGrouping eine neue Gruppe beginnen. Durch mehrfaches Aufrufen von +beginGrouping können Gruppen geschachtelt werden. Mittels -setDuration: kann die Dauer aller folgenden Animationen festgelegt werden. In diesem Falle wird die Dauer auf 5.0 Sekunden gesetzt. Kompilieren Sie Ihr Beispiel und starten Sie es. Klicken Sie auch bewusst während der Animation auf das View. Die gerade laufende Animation wird umgehend beendet und die neue eingeleitet. Dies ist bemerkenswert. Core Animation muss lediglich mitgeteilt werden, was wie animiert werden soll. Um den Rest kümmert sich Core Animation.
506
Textsystem
6
»The only problem with Microsoft is they just have no taste. […] they don’t bring much culture into their products. […] Proportional sized fonts come from typesetting, beautiful books, that’s why one gets the idea.« (Steve Jobs, 1996) Wenn Sie eine textlastige Anwendung entwickeln, sollten Sie sich mit den Grundlagen des Textsystems von OS X und Cocoa auskennen. Eine Einführung in einen der wohl komplexesten, aber auch faszinierendsten Teil von Cocoa erfolgt hier.
SmartBooks
Objective-C und Cocoa – Band 2
Eine Stärke der Macs war schon immer die Typographie. Zu Zeiten, als auf anderen Systemen noch Fettdruck als blaue Schrift angezeigt wurde, war auf dem Mac schon WYSIWYG (What You See Is What You Get) üblich. Die feingliedrige und abstrakte Sicht des Betriebssystems auf das, was als Zeichen auf dem Bildschirm landet, verlangt ein gewisses Umdenken. Eintrainierte Begriffe müssen aufgegeben werden, weil die Mächtigkeit des Textsystems Feinheiten verlangt, die einem reinen Entwickler vielleicht nicht geläufig sind. So wird es manchen Programmierer eines Kommandozeilen-Tools vielleicht verwundern, dass gar nicht Zeichen, sondern Glyphs (Glyphen) ausgegeben werden. (Das hat jetzt nichts mit ägyptischer Lokalisierung zu tun.) Man kann das Textsystem in verschiedene Themenbereiche aufteilen:
•
Das Fontsystem stellt die Schriften zur Verfügung. Dabei werden wir sehen, dass das, was eine Schrift, ein Zeichen, ein Schnitt ist, viel komplexer zusammenhängt, als man sich das als Unicode-Schubser in Xcode vorstellt.
•
Texte müssen mit Attributen wie Fettschrift, Absatzeinzug usw. gespeichert werden (formatierter Text). Hierfür sorgt das Textspeichersystem. Im MVC liegt dieser Bereich auf der Modelebene.
•
Schließlich werden Texte ausgegeben und gesetzt. Dazu dient das Layoutsystem. Es ist auf der View- sowie der Controllerschicht zu verorten.
Diese recht starke Modularisierung hat den Vorteil, dass man gezielt in einzelne Bereiche eingreifen kann. So wird man in aller Regel lediglich mit dem Textview kommunizieren. Neben der hier vorgestellten Cocoa-Schnittstelle zum Textsystem existieren seit Mac OS X 10.5 Leopard noch Core Text sowie schon davor MLTE (Multilingual Text Engine, multilinguale Text-Maschine) und ATSUI (Apple Type Services for Unicode Imaging, Apple Typenservices für Unicode-Darstellung).
•
Core Text stellt die tiefer liegende Maschinerie des Betriebssystems zur Verfügung. Daher kann hiermit gearbeitet werden, wenn umfassend ins Layoutsystem eingegriffen wird und mit Core Graphics Interaktionen stattfinden. Da jedoch die Cocoa-API umfassende Funktionalität offenbart, sollte das sehr selten notwendig sein. Core Text betrifft nur die Textausgabe.
• •
MLTE ist für Carbon-Applikationen bedeutend. Es ermöglicht die Texteingabe. ATSUI ist als früheres zentrales Textsystem und mittlerweile mit Mac OS X 10.6 Leopard veraltet.
Das Textsystem zieht sich durch mehrere Ebenen von Mac OS X:
508
Kapitel 6
Textsystem
Integration des Textsystems ins Betriebssystem
Wie wir sehen, ist auch hier die Funktionalität des Betriebssystems durch das Framework (Cocoa) für unsere Anwendung gekapselt. Es wäre zwar möglich, auf die tieferen Schichten aus der Applikation zuzugreifen. Erforderlich ist das in aller Regel aber nicht. Wenn jedoch aus Performancegründen ohnehin auf tiefer liegende APIs wie Core Graphics zurückgegriffen wird, so bietet sich auch die Benutzung von Core Text an, welches besser passt. Die Ausgabe des Textsystemes lässt sich nach seiner inneren Struktur in zwei Bereiche teilen, nämlich das Zeichen- und Fontsystem und das Layoutsystem. Ersteres sorgt dafür, dass überhaupt Zeichen gespeichert und für sich genommen angezeigt werden können. Das Layoutsystem ordnet demgegenüber mehrere Zeichen (Text) zueinander an. In aller Regel müssen wir hier nicht eingreifen. Aber insbesondere im Hinblick auf das Layoutsystem kann es erforderlich sein, unsere Kundenwünsche unterzubringen.
In Wirklichkeit ist die Trennung weniger scharf als hier.
509
SmartBooks
Objective-C und Cocoa – Band 2
Zeichen- und Fonts Das Fontsystem stellt Zeichen in einem Font zur graphischen Ausgabe zur Verfügung. Es wandelt Codes in graphische Darstellungen um.
Übersicht Insbesondere im Bereich des Font-System gilt es, Begriffe korrekt einzuordnen. Dies liegt darin begründet, dass die Sprache des Entwicklers als Gestaltungslaien deutlich von der des Schriftgestalters abweicht. Missverständnisse sind daher vorprogrammiert:
Zeichen Ein Zeichen (Character) ist nicht etwa die graphische Darstellung eines Buchstabens. Vielmehr handelt es sich um eine reine Informationseinheit. Und zwar um diejenige textuelle Einheit, die nicht mehr geteilt werden kann, also atomar ist. Das sind die uns bekannten Schriftzeichen, aus denen Wörter gebildet werden. Das können aber auch chinesische Schriftzeichen sein, die bereits für ein Wort stehen. Zeichen können Texte bilden, wenn sie in Horden auftreten. Sie haben aber, wie gesagt, nichts mit der Darstellung von Buchstaben zu tun, sondern sind rein abstrakt. Man kann dies mit Zahlen vergleichen: Vielleicht kennen Sie den Spruch »There are 10 kinds of people. Those who understand binary notation, and those who do not.« (»Es gibt 10 Arten von Menschen: diejenigen, die Binärzahlen verstehen und diejenigen, die dies nicht tun.«) Der Witz liegt darin, dass die Buchstabenfolge »10« nicht als zehn in dezimaler Schreibweise gemeint ist, sondern als zwei in binärer Darstellung. Aber auch ohne binäre Darstellung zu bemühen, ist den meisten Menschen nicht klar, dass die folgenden Texte in Wahrheit dieselbe und immer gleiche Zahl zwei darstellen: »2«, »Null Komma Periode 9 plus 1«, »5-3«, »acht Viertel«, … Denn die Zahl, der Wert (also die Information) ist stets gleich (Sie könnten ein Gleichheitszeichen dazwischen schreiben), ihre Darstellung ist jedoch unterschiedlich. Damit man sie aber darstellen kann, müssen weitere Informationen hinzutreten, etwa das Zahlensystem. So verhält es sich auch mit Zeichen: Sie sind lediglich eine Information, die ganz unterschiedlich dargestellt werden kann. Und damit man sie darstellen kann, benötigt man weitere Informationen, wie etwa die Schrift.
Zeichenkodierung Da ein Computer stets Zahlen speichert, nicht jedoch Zeichen, müssen Zeichen in Zahlen übersetzt werden. Dies nennt man die »Zeichenkodierung«. Als Computer 510
Kapitel 6
Textsystem
noch recht wenig leistungsfähig waren, wurde vor allem der ASCII-Zeichensatz verwendet, der lediglich Zahlen für 128 verschiedene Zeichen (teilweise waren die Codes auch mit Steuerinformationen belegt) in Zahlen übersetzte. Es dürfte auf der Hand liegen, dass man damit bei Weitem nicht allen Zeichen auf der Erde eine Zahl zuweisen konnte. Unicode Die führte zur Notwendigkeit eines verbesserten Zeichensystems, welches ursprünglich unter anderem von Mark Davis von Apple angestoßen wurde und in Unicode mündete. Der Unicode soll jedem auf der Erde jemals verwendeten Schriftzeichen nach und nach einen Code zuweisen. Zudem gibt es besondere Steuercodes. OS X arbeitet intern mit dieser Kodierung. Es muss jedoch bedacht werden, dass man bei Kontakt mit der Außenwelt, etwa bei Web-Anwendungen, auf andere Kodierungen treffen kann. Hierfür gibt es Umwandlungsmethoden, die als Parameter das sogenannte Encoding erwarten. Die ersten 128 Zeichen von Unicode entsprechen übrigens den Zeichen im ASCII. Ursprünglich bestand der Unicode aus maximal 65.536 Zeichen, da 16 Bit für die Kodierung vorgesehen waren. Da jedoch diese nicht ausreichten, wurden 17 Ebenen (planes) in entsprechender Größe gebildet. Hierdurch wurde der Unicode 21-bittig.
•
Plane 0: Die Basic-Multilinguale-Plane (BMP, mehrsprachige Basisebene) enthält die klassischen Zeichen des 16-bittigen Unicode-Standards.
•
Plane 1: Die Supplementary-Multilingual-Plane (SMP, mehrsprachige Unterstützungsebene) enthält vor allem historische Schriftzeichen.
•
Plane 2: Die Supplementary-Idiographic-Plane (SIP, ideographische Unterstützungsebene) enthält asiatische Schriftzeichen untergeordneter Bedeutung.
•
Plane 3: Die Tertiary-Ideographic-Plane (TIP, dritte ideographische Ebene) ist bereits zugewiesen, jedoch nicht definiert.
• •
Plane 4-13 sind noch nicht zugewiesen.
•
Plane 14: In diesem Codebereich findet sich die Supplementary-Specialpuprose-Plane (SSPP, Unterstützungsebene für besondere Zwecke), welche wenige Kontrollzeichen festlegt. Plane 15, 16: Diese Bereiche sind als Private-use-Area A und B (PUA, Bereich zur eigenen Verwendung) freigegeben.
511
SmartBooks
Objective-C und Cocoa – Band 2
Die Zeichenübersicht von OS X wirft die Frage auf, wer wohl die DJs in den Diskos von Phaistos sind.
UTF-16 und UTF-8 Das Problem von Unicode ist nun, dass es 21 Bit verwendet – was aus Praktikabilitätsgründen dann 32 Bit würden – und zwar auch dann, wenn es sich um einfache lateinische Zeichen handelte, die bereits im ASCII Zeichensatz enthalten waren. Daher wurden zwei Systeme erfunden, die dafür Sorge tragen, dass häufig verwendete Zeichen wenig Platz im Speicher einnehmen, sozusagen eine Kodierung einer Kodierung.
HILFE Wenn Sie sich mit der binären Zahlendarstellung nicht auskennen, so sind die folgenden Ausführungen vielleicht nicht vollständig verständlich für Sie. Sie finden in der Referenz eine Kurzübersicht bei der internen Darstellung von Ganzzahlen. Ansonsten finden sich haufenweise Erläuterungen zum Binärsystem im Internet. Mit den komplizierteren Komplimentsystemen müssen Sie sich dabei nicht beschäftigen, da es hier um vorzeichenlose Zahlen geht. Die Quintessenz der nachfolgenden Ausführungen werden Sie aber ohnedies nachvollziehen können.
512
Kapitel 6
Textsystem
UTF-16 verwendet dabei 16-bittige Informationseinheiten. Es geht dabei so vor, dass alle Zeichen aus der Ebene 0 als 16-Bit-Zahl unmittelbar aus ihrem Unicode dargestellt werden. Sie sind also im ursprünglichen Unicode und im UTF-16 nicht zu unterscheiden. Befindet sich ein Zeichen aus einer anderen Ebene im Text, so wird dies in zwei Informationseinheiten mit also insgesamt 32 Bit kodiert. 1. Dazu wird der entsprechende Code zunächst um 65536 vermindert, also gewissermaßen die Ebenen 1 bis 16 zu den Sonderebenen 0 bis 15 gemacht. Das führt dazu, dass ein Bit gespart werden kann und der Unicode nun nur noch aus 20 Bit besteht. 2. In einem nächsten Schritt werden diese 20 Bit in zwei Hälften á 10 Bit aufgeteilt. 3. Danach wird den jeweils 10 Bit in der oberen Hälfte das binäre Muster 110110 vorangestellt, um das sogenannte High-Surrogate zu bilden, der unteren Hälfte 110111 für das sogenannte Low-Surrogate. Da die beiden letzten Bits des oberen Bytes vom Ursprungscode belegt werden, liegen die Ergebnisse zwischen hexadezimal D800 und DFFF. Dieser Bereich ist in der Ebene 0 nicht belegt, um so eine Verwechslung mit Zeichen aus dieser Ebene auszuschließen Auf diese Weise entstehen also zwei 16-bittige Informationseinheiten.
Von 21 Bit nach 32 Bit: Die Umwandlung von Unicode zu UTF-16
Ganz ähnlich, nämlich mit Aufteilung und Markern, wird bei UTF-8 verfahren, wobei eine bis maximal vier 8-bittige Informationseinheiten verwendet werden. Dieses noch etwas kompliziertere Verfahren muss hier nicht besprochen werden, denn … Interne Darstellung in Cocoa … Cocoa nutzt als internes Format UTF-16. Dies führt zu Eigenheiten, die überraschend sind:
•
Als Datentyp für die 16-bittige Informationseinheit des UTF-16 wird unichar als 16-bittige Zahl festgelegt. Dies bedeutet, dass abweichend vom Namen hierin eigentlich nicht ein Zeichen des Unicodes gespeichert ist, sondern ein UTF16-Wort. Das fällt nur so lange nicht auf, wie nur die Zeichen aus Ebene 0 verwendet werden, da ja hier Unicode und UTF-16 deckungsgleich sind.
513
SmartBooks
•
Objective-C und Cocoa – Band 2
Bei Zeichen aus den höheren Planes kann das aber nicht mehr stimmen, denn diese weisen 21 Bit auf. Intern wird dies entsprechend UTF-16 durch zwei Werte vom Typen unichar dargestellt. Dies bedeutet, dass die entsprechenden Methoden tatsächlich mitteilen, es handele sich um zwei Zeichen, Zeichen im Sinne von unichar (Codepoints) und UTF-16, nicht Zeichen im Sinne von Unicode:
Wenn Sie mithilfe der Zeichenpalette einen String erzeugen (Drag & Drop nach Xcode), der etwa das Zeichen »CJK Compatibility Ideograph 2F804« enthält, können Sie das leicht testen: NSString* text = @"Ä Ö"; // CJK COMPATIBILITY IDEOGRAPH-2F804 NSLog( @"%@ length: %d", text,[text length] ); NSInteger index; unichar firstChar; for( index = 0; index < [text length]; index++ ) { unichar oneChar = [text characterAtIndex:index]; if( (oneChar > 0xD800) && (oneChar < 0xDBFF) ) { firstChar = oneChar; } else if( (oneChar > 0xDC00) && (oneChar < 0xDFFF) ) { NSLog( @"%d: %4.4X %4.4X", index-1, firstChar, oneChar ); } else { NSLog( @"%d: %4.4X", index, oneChar ); } }
So führt dies tatsächlich zu: >… >… >… >…
Ä 0: 1: 3:
Ö length: 4 00C4 D87E DC04 00D6
Der String besteht also aus vier »Zeichen«, das »Ö« hat den Index 3, nicht 2! Sie sollten übrigens keine Teilstrings erstellen, die ein unichar-Doppel trennen. Dabei kommt nichts Sinnvolles heraus. Um dies zu überprüfen, existiert die Methode -rangeOfComposedCharacterSequencesForRange: (NSString), welche einen vergrößerten Bereich zurückliefert, wenn es eine Trennung gibt. Zeichen, Codepoints und Normalisierung Wir haben zur Vereinfachung mal wieder ein bisschen gelogen. Tut uns leid. Die einzelnen Codes in Unicode stehen gar nicht für Zeichen. Damit ist nicht gemeint, 514
Kapitel 6
Textsystem
dass es freilich Codes für Steuerzeichen usw. gibt, sondern dass manche Zeichen aus mehreren Codes zusammengesetzt werden können. Deshalb nennt der Unicode-Standard seine einzelnen Einträge auch nicht Zeichen, sondern »Codepoint«. Um das zu verstehen, muss man sich einmal besondere Zeichen anschauen: Nehmen Sie unser beliebtes »Ä« (latin capital letter A with diaeresis). Was ist das eigentlich? Ein A-Umlaut. Richtig. Aber ist es ein Zeichen? Oder sind es nicht vielleicht zwei Zeichen, nämlich ein »A« (latin capital letter A) und ein »¨« (combining diaeresis)? Wie man die Sache eben betrachtet. Und daher kann es passieren, dass uns das Ä in beiden Formen über den Weg läuft. Glücklicherweise existieren Normalisierungsvereinbarungen. Unglücklicherweise verwendeten wir gerade den Plural von Normierungsvereinbarung, es existieren also mehrere. Man kann sich nicht darauf verlassen, dass jeder Text, den man so sieht, schon ordentlich normalisiert ist. Sie sollten also Annahmen wie »ein Zeichen, ein Codepoint« besser unterlassen. Übrigens gilt das auch umgekehrt: Es ist nicht immer ganz klar, was ein Zeichen ist. Dieses Problem taucht bei Ligaturen auf, die bereits eigene, neue Zeichen geworden sind. Unserem ß erging es etwa so. Sie werden sicher sagen, dass dies ein Zeichen ist. Ursprünglich handelte es sich indessen um ein besonderes Es, welches mit einem besonderen Zett verschmolzen wurde. Niemand würde das mehr als zwei Buchstaben ansehen. Anders verhält es sich aber zum Beispiel beim niederländischem Ij.
Zahn der Zeit: Ligaturen sind Zeichenpaare, die im Satz verbunden werden. Sie können neue Zeichen werden.
Sortierung Da der Unicode Zeichen thematisch verteilt, kann nicht einfach eine Sortierung anhand der Kodierung vorgenommen werden. Das ist ohnehin schon deshalb nicht möglich, weil im Deutschen etwa Umlaute von manchen Programmen wie ihr Vokal, von anderen wie der Vokal + E, von noch anderen als eigenes Zeichen einsortiert werden. Sie sollten gar keinen Versuch unternehmen, dort einzugreifen, wenn Sie nicht ganz genau wissen, wie Unicode funktioniert. 515
SmartBooks
Objective-C und Cocoa – Band 2
Glyphen Jetzt haben wir Zeichen als Informationseinheit und Codepoints als Kodierungen von Zeichen kennengelernt. Beides betraf die Speicherung von Information. Schließt sich also die Frage an, was denn dann auf dem Bildschirm erscheint: Man nennt es oder vielmehr ihn (im Plural: sie) einen »Glyph«. Diesen erhält man, indem man einem Zeichen Informationen über die Darstellung (Font, Schriftschnitt usw.) hinzufügt und dann vom System den entsprechenden Glyphen ermitteln lässt.
Für jedes Zeichen existieren unterschiedliche Glyphen.
Dabei modelliert das Textsystem mit dem Typen NSGlyph einen Glyphen zunächst nur als Index in dem Font. Dies findet sein Bewenden darin, dass ein Font nicht alle Zeichen des Unicodes enthalten muss. Die Indizes im Font sind also gewissermaßen insoweit zusammengepresst, als unbelegte Zeichen herausfallen. Sie können auch eine andere Reihenfolge haben. Aus diesem Glyphen kann man dann etwa den Bezier-Pfad extrahieren, wobei dieser wieder von der Größe abhängig ist. Sie hatten das ja schon im ersten Kapitel gesehen. Auf den ersten Blick scheint dies erstaunlich, da man innerhalb eines Fonts erwarten sollte, dass die Form eines Zeichens größenunabhängig ist. Allerdings kann man das auch anders sehen, wenn man etwa Serifen (die kleinen Spitzelchen an manchen Buchstaben) hat. Es ist daher dem Schriftgestalter unbenommen, ein und denselben Glyphen bei verschiedenen Größen unterschiedlich zu gestalten. Umgekehrt kann es vorkommen, dass ein Zeichen zwei Glyphen verursacht. So kann es etwa passieren, dass das Zeichen »ä« mit den zwei Glyphen a und ¨ gesetzt wird, und zwar unabhängig davon, ob es in ein oder zwei Zeichen kodiert wurde. Also auch hier ist jede Annahme gefährlich, dass ein Zeichen zu einem Glyphen führt.
Fonts Der Font (Schriftschnitt) bezeichnet eine Sammlung von Glyphen zur Abdeckung von Zeichen eines Schriftschnittes. So ist etwa »Helvetica, 10 pt, kursiver Schriftschnitt, fetter Schriftschnitt, normale Breite« ein Font. Mehrere zusammengehörige Fonts bilden eine Font-Family (Schrift(schnitt)familie). Dabei ist das allerdings nicht so wie in der Schriftgestaltung zu verstehen, dass alle Fonts zu einer Familie gehören, die deutliche gestalterische Gemeinsamkeiten aufweisen. Vielmehr wären dies (nur) alle Helvetica-Fonts, unabhängig von ihrer Größe und ihrem Schnitt. 516
Kapitel 6
Textsystem
Die Unterscheidung innerhalb der Familie erfolgt mit sogenannten Font-Traits wie kursiv, breit usw. Es ist jedoch möglich, Fonts nach bestimmten gestalterischen Merkmalen auszuwählen. Hierzu dient der Font-Descriptor der Klasse NSFontDescriptor. Dort lässt sich etwa nach Fonts suchen, die keine Serifen aufweisen. Dies wird durch den Typen NSFontFamilyClass modelliert, was dann dem Begriff »Schriftfamilie« entspricht, wie ihn ein Gestalter verwendet. Fonts werden durch Instanzen der Klasse NSFont dargestellt. Für den Zugang zur Fontverwaltung des Systems existiert die Klasse NSFontManager.
Fontmanager Die zunächst einfachste Aufgabe ist es, Fonts einzusetzen, also dem User die Möglichkeit zu geben, Fonts auszuwählen. Hierzu dienen die Klassen NSFont, NSFontManager und NSFontPanel. Wie Sie in den meisten textintensiven Anwendungen sehen können, gibt es ein Fontmenü und einen Fontpanel. Tatsächlich ist die Integration kinderleicht, da alles im Hintergrund erledigt werden kann. Erstellen Sie ein neues Projekt FontSelector vom Typ Cocoa Application ohne Unterstützung von Core Data und Dokumenten. Öffnen Sie den MainMenu.xib. Sie werden schon in den bisherigen Projekten bemerkt haben, dass in der Menüzeile automatisch ein Fontmenü enthalten ist und in dem Nib-Fenster ein Font-Manager auftaucht. Ziehen Sie ein großes Text View (nicht: Text-Field) aus der Library des Interface Builders in das Anwendungsfenster. Speichern Sie Ihre Arbeit und starten Sie das Programm. Ohne weiteres Zutun kann der User jetzt im Fontmenü für das Textview verschiedene Fonts und FontTraits auswählen.
HILFE Sie können das Projekt in diesem Zustand als Projekt »FontSelector 1« von der Webseite herunterladen.
517
SmartBooks
Objective-C und Cocoa – Band 2
Vorgefertigt: das Fontmenü
Fontmanager und Fontmenu Gut, aber vielleicht etwas zu einfach. Betrachten wir die Sache etwas genauer. Der Fontmanager ist die zentrale Eintrittskarte in die Fontverwaltung des Systems. Das Menü kommuniziert auf zweierlei Weise mit dem Fontmanager. Um das zu erkennen, schalten Sie bitte im Inspector auf das Connections-Pane.
•
Für manche Aktionen wie Show Fonts oder Underline existieren eigene Actionmethoden wie -orderFrontFontPanel: bzw. -underline:. Diese werden dann in gewohnter Manier mit dem Menüeintrag verbunden.
•
Wenn Sie stattdessen im Menü einmal Bold oder Italic auswählen, werden Sie sehen, dass stets dieselbe Actionmethode -addFontTrait: ausgeführt wird. Da stellt sich die Frage, woher der Fontmanager weiß, was er genau tun soll. Des Rätsels Lösung liegt im Tag des Menüs. Es enthält einen Code. Bitte schalten Sie den Inspector einmal auf das Attributes-Pane und schauen Sie sich den Eintrag Tag für die genannten Menüeinträge an.
AUFGEPASST Entgegen der Dokumentation handelt es sich bei den Werten nicht um die Font-Traits, sondern um Konstanten des Typen NSFontAction. Fontmanager und Code Der Fontmanager ist ein Singleton, den wir also von überall her mit +sharedFontManager (NSFontManager) ermitteln können. Davon ausgehend können wir den aktuellen Font abholen und diesen anzeigen. Auch dies wollen wir an einem Beispiel vornehmen: Zunächst fügen wir dem Application-Delegate eine neues Outlet und eine neue Actionmethode -showFontName: hinzu …: 518
Kapitel 6
Textsystem
@interface FontSelectorAppDelegate : NSObject { NSWindow* window; IBOutlet NSTextField* displayNameLabel; IBOutlet NSTextField* fontNameLabel; IBOutlet NSTextField* familyNameLabel; } @property( assign ) IBOutlet NSWindow *window; - (IBAction)showFontName:(id)sender; @end
… dir wir in FontSelectorAppDelegate.m implementieren: @synthesize window; - (IBAction)showFontName:(id)sender { NSFontManager* manager = [NSFontManager sharedFontManager]; NSFont* font = [manager selectedFont]; [displayNameLabel setStringValue:[font displayName]]; [fontNameLabel setStringValue:[font fontName]]; [familyNameLabel setStringValue:[font familyName]] ; }
Sie wechseln sodann bitte wieder zu MainMenu.xib und ziehen einen Button und drei Label in das Anwendungsfenster. Den Button verbinden Sie mit der neuen Methode und die Outlets des Delegates mit den Labels. Wenn Sie nach Übersetzung und Programmstart im Fontpanel einen Font auswählen, können Sie dessen Namen mit einem Klick auf den Button anzeigen lassen. Wählen Sie bitte auch abweichende Schriftschnitte wie kursiv und fett aus. Gut geeignet ist etwa Goudy Old Style. Sie sehen: Während der Display-Name ein nutzerlesbarer String ist, handelt es sich bei dem Fontnamen um die echte Bezeichnung des Fonts. Nur diese identifiziert den Font auch wirklich. Bei dem Family-Name indessen handelt es sich um den Grundnamen ohne Berücksichtigung von Schnitten. Natürlich können Sie einfach das Programm um die Anzeige weiterer Eigenschaften des Fonts erweitern. Schauen Sie dazu in die Dokumentation zu NSFont und probieren Sie etwas herum. Wir wollten hier nur die »Eintrittskarte« demonstrieren.
519
SmartBooks
Objective-C und Cocoa – Band 2
HILFE Sie können das Projekt in diesem Zustand als Projekt »FontSelector 2« von der Webseite herunterladen.
Fonts, Ausgabe und Bemaßung Zuweilen ist es notwendig, selbst Text auf den Bildschirm zu schreiben. Wir haben das ja auch schon gemacht, indem wir in dem Kartenspiel die Bezierpfade für einzelne Zeichen ermitteln ließen. Prinzipiell kann man vier Arten unterscheiden:
•
Es existiert eine Kategorie NSStringDrawing von NSString mit Methoden wie -drawAtPoint:attributes:, mit der sich Strings unmittelbar zeichnen lassen oder sich die Größe eines Strings ermitteln lässt. Entsprechendes gilt für die gleich noch zu besprechende Klasse NSAttributedString. Diese Methoden sind jedoch von der Performance her sehr nachteilig, da der noch zu besprechende Textstapel für jeden Aufruf verwendet wird. Dafür sind sie jedoch in der Handhabung extrem simpel.
•
Wie bereits gezeigt, kann es unter Umständen sinnvoll sein, einer Basisklasse, die bereits Text ausgeben kann, die Ausgabe zu überlassen. Das hatten wir bei den Cells so gehandhabt. Freilich hat dies den Nachteil, dass es nur bei bestimmten Basisklassen funktioniert.
•
Im ersten Kapitel hatten wir die Möglichkeit gesehen, uns Bezierpfade für Glyphen geben zu lassen und diese zu zeichnen. Dies werden wir hier etwas vertiefen, da sich so sehr schön die Bemaßung eines Fonts demonstrieren lässt.
•
Schließlich besprechen wir im Anschluss die Möglichkeit der Textausgabe mit dem Layoutsystem, welches dann auch komplexe Operationen wie Absatzformate und Umbruch erlaubt.
Erstellen Sie ein neues Projekt CIcyxView als Cocoa Application, wiederum ohne Support für Dokumente und Core Data. Wir werden uns einen View programmieren, der einfach den Text »CIcyx« an die Größe des Views angepasst auf den Bildschirm schreibt. Dabei kommen wir in Kontakt mit einzelnen Eigenschaften eines Fonts, werden Glyphs handhaben und kehren wieder zu Bezierpfaden zurück. Erstellen Sie in dem neuen Projekt zunächst eine Subklasse CIcyxView in Ableitung von NSView. Wechseln Sie nun in die Implementierungsdatei CIcyxView.m. Zunächst legen wir eine Konstante fest, welche dafür sorgt, dass die Ausgabe mit etwas Rand erfolgt: 520
Kapitel 6
Textsystem
const CGFloat border = 0.1; @implementation CIcyxView
Wir formulieren uns eine einfache Hilfsmethode und erfüllen die -drawRect:-Methode, welche den Text und seine Bemaßung ausgibt, mit Leben: - (void)strokeHorizontalLineFrom:(CGFloat)minX width:(CGFloat)width at:(CGFloat)y transform:(NSAffineTransform*)transform { NSBezierPath* path = [NSBezierPath bezierPath]; [path setLineWidth:0.0]; [path moveToPoint:NSMakePoint( minX, y )]; [path relativeLineToPoint:NSMakePoint( width, 0.0 )]; [path transformUsingAffineTransform:transform]; [path stroke]; } - (void)drawRect:(NSRect)dirtyRect { // Hintergrund weiß [[NSColor whiteColor] set]; NSRectFill( dirtyRect ); // Berechne Zeichenfläche NSRect drawRect = self.bounds; drawRect = NSInsetRect( self.bounds, NSWidth( drawRect ) * border, NSHeight( drawRect ) * border ); // Hole Standardfont in Standardgröße NSFont* font = [NSFont userFontOfSize:0.0]; …
Die letzte Zeile holt den Standardfont für Nutzerausgaben in der Standardgröße. Andere Ideen wären etwa +fontWithName:size: oder +fontWithDescriptor: size: (beide NSFont), wobei als Name nicht etwa Helvetica ausreicht, sondern der komplette Langname mit Zusätzen für fetten (bold) bzw. kursiven (italic, oblique) Schriftschnitt enthalten sein muss. Es handelt sich also um den Fontnamen aus dem letzten Projekt. Übrigens deutet die Notwendigkeit, selbst den Fontnamen zu bestimmen, häufig auf einen Fehler im Design hin, da die Fontauswahl ja vom Benutzer stammen sollte, nicht aus der Anwendung. 521
SmartBooks
Objective-C und Cocoa – Band 2
… // Hole CGFloat CGFloat CGFloat CGFloat CGFloat
die wichtigsten Font-Eigenschaften ascender = [font ascender]; descender = [font descender]; fontHeight = ascender - descender; capHeight = [font capHeight]; xHeight = [font xHeight];
…
Hier werden dann vier Eigenschaften des Fonts geholt. Wir werden das gleich anhand der Ausgabe näher besprechen. Beachten Sie nur schon hier die Berechnung für die Höhe des Fonts: Der Ascender ist der größte Abstand zur Basislinie, die auf der Höhe 0,0 liegt. Der Descender ist ebenfalls der größte Abstand, allerdings für die Bestandteile von Zeichen, die unterhalb der Basislinie liegen. Er wird in negativen Werten ausgedrückt, so dass obige Subtraktion betragsmäßig eine Addition darstellt. … // Hole die Glyphen NSGlyph glyphs[5]; glyphs[0] = [font glyphWithName:@"C"]; glyphs[1] = [font glyphWithName:@"I"]; glyphs[2] = [font glyphWithName:@"c"]; glyphs[3] = [font glyphWithName:@"y"]; glyphs[4] = [font glyphWithName:@"x"]; …
Bei der ersten Zeile handelt es sich um ein C-Array, welches in der Referenz näher erläutert wird. Sie können sich die einfache Anwendung hier so vorstellen, dass die Variable glyphs fünfmal existiert. Es sei zudem angemerkt, dass hier wirklich die einzelnen Glyphs aus dem Font gelesen werden, so dass als Antwort tatsächlich immer nur ein einzelner Glyph geliefert wird. Natürlich wäre es möglich, dass der entsprechende Font keinen passenden Gly:NSMakePphen enthält. Dann würde -1 als Glyph geliefert, was man abfragen könnte. (Sie erinnern sich: Glyphen sind Indexe, nicht graphische Repräsentationen.) Wir glauben hier einfach, dass ein Standardfont Glyphen für die hier verwendeten Zeichen enthält. … // Erzeuge einen Pfad für den Text NSBezierPath* glyphsPath = [NSBezierPath bezierPath]; [glyphsPath moveToPointoint( 0.0, 0.0 )]; [glyphsPath appendBezierPathWithGlyphs:glyphs count:5 inFont:font];
522
Kapitel 6
Textsystem
CGFloat minX = NSMinX( [glyphsPath bounds] ); CGFloat width = NSWidth( [glyphsPath bounds] ); …
Aus den Glyphen wird dann ein Bezierpfad erzeugt. Der Ursprung liegt bei 0.0, was bei unserer Schreibrichtung dem linken Ende der Basislinie entspricht.
GRUNDLAGEN Das ist natürlich schon wieder eine Annahme, die natürlich nur für unseren Sprachraum gilt. Sie bekommen daher vielleicht einen Eindruck von der Komplexität des Layoutsystems von Cocoa/OS X, wenn Sie bedenken, dass es unterschiedliche Schreibrichtungen gibt. Ein besonders eindrucksvolles Beispiel lieferte Max Seelemann auf der Macoun mit einerm hebräischen Text, der ein lateinisches Zitat enthielt. Hier änderte sich die Schriftrichtung innerhalb einer Zeile, so dass eine durchgehende Selektierung des Textes dazu führte, dass der blaue Selektionshintergrund unterbrochen wurde. Die Schreibrichtung drehte sich ja beim Wechsel von Hebräisch zu Lateinisch, so dass die Selektion jetzt »von der anderen Seite« erweitert werden musste. Irre! … // Erstelle eine Transformation, die in der Höhe passt. NSAffineTransform* transform = [NSAffineTransform transform]; [transform translateXBy:0.0 yBy:NSMinY( drawRect )]; CGFloat scale = NSHeight( drawRect ) / fontHeight; [transform scaleXBy:scale yBy:scale]; [transform translateXBy:0.0 yBy:-descender]; [glyphsPath transformUsingAffineTransform:transform]; …
Dies ist eine Transformation, um den Text in der Größe an den View anzupassen und die Basislinie nach oben zu verschieben. … // Zeichne die Bounds des Bezierpfades [[NSColor lightGrayColor] set]; NSRectFill( [glyphsPath bounds] ); …
Die letzte Zeile sieht unscheinbar aus, ist es aber nicht. Bitte beachten Sie, dass hier wirklich die geometrischen Ausmaße des erzeugten Pfades verwendet werden. Das hellgraue Rechteck umfasst also genau den hier erzeugten Pfad und hat in seiner Dimensionierung nichts mit dem Font an sich zu tun! 523
SmartBooks
Objective-C und Cocoa – Band 2
… // Zeichne die Glyphen als Bezierpfade [glyphsPath setLineWidth:0.0]; [[NSColor whiteColor] set]; [glyphsPath fill]; [[NSColor blackColor] set]; [glyphsPath stroke]; // Zeichne Metriken [[NSColor blackColor] set]; [self strokeHorizontalLineFrom:minX width:width transform:transform]; [self strokeHorizontalLineFrom:minX width:width transform:transform]; [self strokeHorizontalLineFrom:minX width:width transform:transform]; [self strokeHorizontalLineFrom:minX width:width transform:transform]; [self strokeHorizontalLineFrom:minX width:width transform:transform];
at:0.0 at:xHeight at:capHeight at:ascender at:descender
}
Hier werden jetzt also die verschiedenen Bemaßungen gezeichnet. Um das Ergebnis zu sehen, müssen Sie noch in MainMenu.xib wechseln. Wählen Sie zunächst das Anwendungsfenster im Nib-Fenster aus und geben Sie als Größe 750 × 380 ein. Ziehen Sie nun einen Customview in das Fenster und stellen Sie dessen Autosizing so ein, dass es eine Änderung des Fensters mitmacht. Schließlich setzen Sie im Identity-Pane die neue Klasse CIcyxView. Übersetzen und starten Sie das Programm. Sie werden folgende Ausgabe erhalten:
Die geometrische und gestalterische Bemaßung von Zeichen
524
Kapitel 6
Textsystem
Schauen wir uns das näher an: Das graue Bounds-Rechteck des Pfades umschließt (von Anti-Aliasing mal abgesehen) exakt den Text. Aber was ist mit unseren schlauen Linien? Das »x« steht noch auf der Basislinie und schließt tatsächlich mit der x-Height ab. Genau das bedeutet es übrigens: Es handelt sich um die Höhe eines kleinen X. Auch das große I zeigt uns noch, was mit Cap-Height gemeint ist: Die Größe eines Großbuchstabens. Aber wo ist etwa das »C« hingerutscht? Das steht oben und unten über. Der Grund ist einfach: Der fertige Font soll so aussehen, dass die entsprechenden Zeichen eben auf der Basislinie stehen und in der Höhe so erscheinen wie die Cap-Height bzw. x-Height. Würden jedoch Buchstaben mit »runden Enden« die Basislinie so eben berühren, so entstünde der Eindruck, als schwebten sie. Der optische Eindruck hängt nämlich nicht auf dem letzten Pixel einer Rundung, da dieses kein optisches Gewicht mehr hat. Um das auszugleichen, wird es etwas unter die Basislinie gezogen. Im Schriftbild erscheint es dann so, und nur so(!), wie auf der Basislinie stehend. Bei Buchstaben mit geradem Abschluss wie dem »x« und dem »I« ist der Ausgleich nicht erforderlich.
GRUNDLAGEN Etwas optisch gerade oder in eine Mitte zu setzen, bedeutet eben nicht, dass es sich auch geometrisch so verhält. Man kann das nicht berechnen. Der Schriftgestalter muss dies vielmehr in mühevoller Kleinarbeit Zeichen für Zeichen von Hand ausprobieren. Und jetzt ist auch klar, warum eine simple Vergrößerung der Bezierpfade eines Fonts nicht immer den richtigen Eindruck erzeugt. Diese Kleinarbeit macht einen guten Font teuer. Wenn Sie sich das nächste Mal in Athen befinden, besuchen Sie doch die Akropolis mit einem Zollstock bewaffnet. Die dort optisch äquidistant gebauten Säulen haben in Wahrheit ganz unterschiedliche Abstände: Unser Auge nimmt eben einen inneren Abstand anders wahr als einen am Rande. Das muss ausgeglichen werden. Wussten schon die alten Griechen … (Die damals allerdings vermutlich recht jung waren.) Die Lehre, die Sie als Programmierer daraus ziehen, ist, dass Sie eben nicht einen View mit Text einfach nach den inneren Fontmaßen berechnen können. Lediglich die unterste und die oberste Linie sind fest. Alles andere sind optische Hilfslinien, nicht mathematische Exaktheit.
Layoutsystem Eine nicht einfache Aufgabe stellt das Problem dar, Text zu layouten. Letztlich geht es hierbei weniger um das Aussehen einzelner Buchstaben, welches wir ja gerade besprochen haben. Wir nennen das mal schlagwortartig »Rendering«. Vielmehr 525
SmartBooks
Objective-C und Cocoa – Band 2
behandeln wir die großflächige Anordnung von Buchstaben und Wörtern, also eben das Layout. In der Dokumentation von Apple wird dies mehr oder weniger scharf als »Textsystem« bezeichnet. Wir finden den Ausdruck »Layoutsystem« passender, weil ja auch Zeichen irgendwie mit Text zusammenhängen. Bisher haben wir es uns recht einfach gemacht und einfach View-Objekte (NSTextView) im Interface Builder ins Fenster gezogen. Diese konnten zwar mehrzeilig sein und wurden automatisch umgebrochen. Aber bereits ein Blick in die Zeitung verrät, dass es natürlich viel komplexere Layout-Anforderungen gibt: Spalten, Layout um Graphiken, Textrichtungen usw. usf.
GRUNDLAGEN Dass es bisher so einfach ging, liegt daran, dass ein Textview in seiner - initWithFrame:-Methode selbst heimlich ein Textsystem aufbaut. Dies haben wir einfach mitbenutzt.
Layoutstapel Um all dies zu bewerkstelligen, baut Cocoa einen Stapel von Objekten, die für das Layout zuständig sind.
Der Stapel zur Layout-Erzeugung
526
Kapitel 6
Textsystem
Textstorage Ganz unten befindet sich ein Textstorage-Objekt von der Klasse NSTextStorage, welches den eigentlichen Text enthält. NSTextStorage ist von NSMutableAttributedString abgeleitet, diese wiederum von NSAttributedString und diese wiederum – nein, nicht von NSString – von NSObject. Es handelt sich sozusagen um das Model. Layoutmanager, Glyphgenerator, Typesetter Jedem Textstorage können ein oder mehrere Layoutmanager zugeordnet werden. Der Layoutmanager übernimmt die zentrale Arbeit beim Textsatz. Er erzeugt die Glyphen, bricht Zeilen um, kümmert sich um die Verwaltung der Textattribute usw. usf. Er ist der Kitt zwischen Textstorage und der View-Ebene. Er ist im MVC also der Controller.
POWER Hier zeigt sich wieder die gleiche Denke: Der Layoutmanager organisiert die Anordnung. Wenn ich also verschiedene Views haben möchte, die den gleichen Text in verschiedener Weise anordnen, so werde ich mehrere Layoutmanager verwenden. Habe ich lediglich kleinere Unterschiede in der Darstellung, so kann ich auf höheren Ebenen des Stapels eingreifen. Damit er diese Aufgaben erledigen kann, bedient sich der Layoutmanager des Glyphgenerators und des Typesetters. Der Glyphgenerator erzeugt aus dem Strom der kodierten Zeichen einen Strom an Glyphen. Diese sind aber wie im vorangegangenen Abschnitt isoliert und haben noch keinen Bezug zum Text und zu den Nachbarn. Der Glyphgenerator ist also sozusagen der Buchstabenkasten des Setzers. Wir haben vorhin also vielleicht einen ganz simplen Glyphgenerator programmiert. Diese Glyphen müssen dann entlang der zu besetzenden Fläche angeordnet werden. Hierfür sorgt der Typesetter, also der Schriftsetzer in der Druckerei. Er kann dabei den Glyphenstrom ändern, wenn dies notwendig wird.
Der Glpyhgenerator erzeugt die Zeichen, die der Typesetter anordnet.
527
SmartBooks
Objective-C und Cocoa – Band 2
Es ist möglich, mehrere Layoutmanager zu einem Textstorage hinzuzufügen. Dann kann der Text auf verschiedene Weise dargestellt werden. Textcontainer, Textview Jeder Layoutmanager verwaltet wiederum Textcontainer. Textcontainer sind Bereiche, in denen Text platziert werden kann. Im einfachsten Falle handelt es ist einfach um ein Rechteck. Es können mehrere solcher Bereiche vorhanden sein, etwa, wenn man das Dokument in Seiten darstellt, die einzelnen Seiten. Der Layoutmanager kommuniziert mit ihnen vor allem dadurch, dass er Anfragen nach Rechtecken stellt, in denen Text untergebracht werden kann. Es stellt sich die Frage nach der Notwendigkeit. Immerhin existiert ja ein Textview, welches den Text darstellt. Und zwischen Textcontainer und Textview besteht auch noch eine 1:1-Beziehung, jeder Textcontainer hat also seinen Textview. Der Grund besteht in der Anpassung: Bei einem Textcontainer, also der beschreibbaren Fläche, kann es sinnvoll sein, Subklassen zur Anpassung zu erzeugen. Der Typesetter des Layoutmanagers setzt also nur dort seine Buchstaben herein, wo es der Textcontainer erlaubt. Die Ableitung des Textcontainers ist erforderlich, wenn man andere Anordnungen als Rechtecke haben möchte. Die Aufteilung zwischen Textview und Textcontainer sorgt dafür, dass nicht gleich ein Textview abgeleitet werden muss, der ja nur innerhalb der durch den Textcontainer bestimmten Form Texte darstellen soll. Es liegt also einfach eine funktionale Aufteilung zwischen Zeichenplatzierung und Zeichenausgabe vor, um Anpassungen besser zu modularisieren. Es ist möglich, mehrere Textcontainer-Textview-Paare einem Layoutmanager hinzuzufügen. Dann wird der Text entlang dieser Paare gesetzt, also zunächst im ersten und, falls dieser nicht ausreicht, im zweiten usw. Hiermit sind Layouts mit mehreren Seiten oder mehreren Spalten pro Seite oder die Kombination davon ebenso möglich wie die Unterstützung von mehreren Textrahmen. Speicherverwaltung Das Retaining der Elemente erfolgt von unten nach oben: Der Textstorage hält den Layoutmanager, dieser hält den Textcontainer.
Layoutstapel erstellen Aber das ist alles graue Theorie. Machen wir uns an die Arbeit mit etwas Sinnvollem. Dabei ist es nicht einmal ansatzweise möglich, die unendlichen Aspekte des Textsystems zu besprechen. Wir werden aber ein typisches Problem lösen: angepassten Textsatz. 528
Kapitel 6
Textsystem
Bevor wir überhaupt damit anfangen, im System herumzufummeln, muss das, was ein Textview automatisch macht, von uns per Hand erstellt werden: Wir müssen einen Layoutstapel zusammenschrauben. Das Ganze hat selbst also keinen Zweck und keine Funktionalität. Es ist nur unsere Eintrittskarte. Und als Fingerübung beginnen wir damit. Erzeugen Sie ein neues Projekt LayoutStack von der Vorlage Cocoa Application ohne Unterstützung für Dokumente und Core Data. Im Application-Delegate fügen Sie im Header ein neues Outlet ein: @interface LayoutStackAppDelegate : NSObject { NSWindow *window; IBOutlet NSView* placeholder; NSTextStorage* storage; } @property (assign) IBOutlet NSWindow *window; @property( retain ) NSTextStorage* storage;
Öffnen Sie MainMenu.xib und ziehen Sie ein Customview aus der Library des Interface Builders ins Anwendungsfenster. Setzen Sie das Autosizing so, dass die Vergrößerung des Fensters in den View geht. Verbinden Sie das Outlet placeholder des Applikationsdelegates mit dem Customview. Wir werden den kompletten Stapel bis zum View hin aufbauen, auch wenn das erst einmal vielleicht nicht notwendig ist.
TIPP In vielen Beispielen wird dann der im Programm erzeugte View als contentView des Fensters gesetzt. Dies ist zwar einfach, hat aber den Nachteil, dass viele Einstellungen nicht im Interface Builder gemacht werden können. Außerdem ist damit das Fenster dann endgültig belegt. Unsere Lösung belässt die Bemaßung des Views und liest Eigenschaften aus, die es dem neuen View gibt. Öffnen Sie nun die Datei LayoutStackAppDelegate.m. Dort bauen Sie den Layoutstapel und setzen den View anstelle des Customviews in das Fenster.
GRUNDLAGEN Die im Interface Builder ins Fenster gezogenen Textviews sind Subviews eines Scrollviews. Dies ist auch meist sinnvoll. Sie gehen dann im Prinzip ebenso vor, setzen bloß den Subview des Scrollviews neu. Für unsere Arbeit am Layout-System ändert sich nichts. Und wir wollen das hier nicht unnötig verkomplizieren. 529
SmartBooks
Objective-C und Cocoa – Band 2
@implementation LayoutStackAppDelegate @synthesize window; @synthesize storage; - (void)applicationDidFinishLaunching:(NSNotification*)notif { // Stufe 1: Textstorage erzeugen self.storage = [[[NSTextStorage alloc] init] autorelease]; // Stufe 2: LayoutManager erzeugen und an den Storage hängen NSLayoutManager* layoutManager = [[[NSLayoutManager alloc] init] autorelease]; [self.storage addLayoutManager:layoutManager]; // Stufe 3: Text-Container, der als Fläche ein Rechteck // in der Größe des Views hat NSRect frame = placeholder.frame; NSTextContainer* container = [[[NSTextContainer alloc] initWithContainerSize:frame.size] autorelease]; [container setWidthTracksTextView:YES]; [container setHeightTracksTextView:YES]; [layoutManager addTextContainer:container]; // Stufe 4: Ein Textview für die Anzeige in der Größe des // Containers NSTextView* textView = [[[NSTextView alloc] initWithFrame:frame textContainer:container] autorelease]; [textView setAutoresizingMask:[placeholder autoresizingMask]]; // Views Austauschen [[placeholder superview] addSubview:textView]; [placeholder removeFromSuperview]; }
Die erklärungsbedürftigen Methoden -setWidthTracksTextView: und -setHeightTracksTextView: (beide NSTextContainer) sorgen dafür, dass bei einem Resize des
530
Kapitel 6
Textsystem
Textviews der Textcontainer sich automatisch anpasst. Dies bedeutet nicht, dass sich bei Einfügung von zu viel Text die Größe des Textviews ändert. Um eine Vergrößerung bei der Texteingabe zu erzielen, existieren für den Textview die Methoden -setVerticallyResizable: und -setHorizontallyResizable:, welche seine Größe anpassen. Dies ist vor allem in vertikaler Richtung interessant, wenn sich der Textview in einem Scrollview befindet. Niemals dürfen beide Möglichkeiten gleichzeitig eingeschaltet sein, da sich dann Container und View bis in die ewigen Jagdgründe gegenseitig resizen. Denn der Textcontainer vergrößert sich dann automatisch bei zusätzlichem Text und teilt dies dem View mit. Ist eingestellt, dass die Vergrößerung des Views an den Textcontainer übermittelt werden soll, wird der Textview wieder den Textcontainer anpassen, der daraufhin … Starten Sie das Programm. Wir haben einen hässlichen Textview, der aber das Erwartete tut.
HILFE Sie können das Projekt in diesem Zustand als Projekt »LayoutStack 1« von der Webseite herunterladen. Probieren Sie es ruhig einmal aus, die Resizing-Parameter auf NO zu setzen. Dann bleibt der Textcontainer in seiner Größe konstant, wenn Sie den Textview in seiner Ausdehnung ändern. Bitte beachten Sie, dass NSTextView verschiedene Methoden enthält, die die Lage des Textcontainers im Textview beeinflussen.
Textcontainer
Textview
Der Textcontainer passt sich hier nicht dem Textview an.
531
SmartBooks
Objective-C und Cocoa – Band 2
Textview Das Textview ist für die Ausgabe zuständig und verwaltet die Selektion. Selektion In einem Textview können Bereiche selektiert sein. Diese Bereiche lassen sich als Array mit -selectedRanges ermitteln. Dabei sind drei Fälle zu unterscheiden:
•
Es ist kein Bereich selektiert: Dann wird lediglich ein Bereich zurückgeliefert, der als location die aktuelle Cursorposition und als length 0 enthält.
•
Es ist ein Bereich selektiert: Dieser bildet mit Position und Länge den einzigen Eintrag im Array.
•
Es sind mehrere Bereiche selektiert: Die Bereiche sind aufsteigend sortiert und hängen nicht zusammen. Sollten also zwei aufeinander folgende Bereiche vom Benutzer selektiert worden sein, so werden diese zu einem Bereich verschmolzen.
Ausgabe Machen wir daraus mal etwas Nützliches. Wir wollen ein View bauen, welches in der Mitte einen Kreis hat. Offenkundig müssen wir als Allererstes das View dazu überreden, einen Kreis in der Mitte zu zeichnen. Dieser stammt ja nicht aus dem Text, sondern ist eine reine Anzeige-Angelegenheit. Leiten wir also einen Textview ab, wobei Sie als Vorlage Objective-C subclass und NSView verwenden. @interface CircleTextView : NSTextView { } - (NSRect)circleRect; @end
Die Methode dient dazu, den Kreis einheitlich zu bestimmen. Wir kommen darauf beim Textcontainer zurück. In der Implementierung CircleTextView.m überschreiben wir die Methode -drawRect: @implementation CircleTextView - (NSRect)circleRect { NSRect circleRect = self.bounds;
532
Kapitel 6
Textsystem
if( NSWidth( circleRect ) > NSHeight( circleRect ) ) { circleRect.size.width = circleRect.size.height; circleRect.origin.x = (NSWidth( self.bounds ) - NSWidth( circleRect )) / 2; } else { circleRect.size.height = circleRect.size.width; circleRect.origin.y = (NSHeight( self.bounds ) - NSHeight( circleRect )) / 2; } circleRect = NSInsetRect( circleRect, NSWidth( circleRect ) * 0.1, NSHeight( circleRect ) * 0.1 ); return circleRect; } - (id)initWithFrame:(NSRect)frame textContainer:container { self = [super initWithFrame:frame textContainer:container]; … } - (void)drawRect:(NSRect)dirtyRect { [super drawRect:dirtyRect]; NSRect circleRect = [self circleRect]; NSBezierPath* circle = [NSBezierPath bezierPathWithOvalInRect:circleRect]; [[NSColor blueColor] setStroke]; [circle stroke]; }
Wie Sie unschwer erkennen können, wird einfach die Zeichenroutine der Superklasse NSTextView aufgerufen und hiernach ein blauer Kreis in der Mitte gezeichnet. Beachten Sie bitte, dass die besondere Ausgabe (Kreis) hier im Textview erfolgt, die Platzierung der Zeichen aber mit keiner Zeile erwähnt wird. Schön. Nun muss im Application-Delegate freilich noch ein View unserer neuen Subklasse erzeugt werden. Dazu ändern Sie dort die folgende Zeile: #import "CircleTextView.h" @implementation LayoutStackAppDelegate … - (void)applicationDidFinishLaunching:(NSNotification*)notif
533
SmartBooks
Objective-C und Cocoa – Band 2
{ … CircleTextView* textView = [[[CircleTextView alloc] initWithFrame:frame textContainer:container] autorelease]; [textView setAutoresizingMask:[placeholder autoresizingMask]]; … }
Wenn Sie jetzt das Programm starten und reichlich Text eingeben, bemerken Sie aber auch sogleich das Problem: Der Text wird wie bisher flächig gesetzt, der Kreis übermalt dies lediglich.
Textcontainer Der Grund ist offensichtlich. Wir malen einen Kreis. Wir sorgen nicht dafür, dass der Kreis beim Umbruch berücksichtigt wird. Dies müssen wir also noch ergänzen. Wie Sie bereits wissen, sorgt der Textcontainer für die Bestimmung der beschreibbaren Bereiche. Wir müssen diesen also ableiten. Wohlan: Erzeugen Sie eine Subklasse CircleTextContainer der Klasse NSTextContainer in Xcode mit File | New File:, wobei Sie wieder NSObject als Ausgangsklasse verwenden. Bitte die Basisklasse eingeben: #import "CircleTextView.h" @interface CircleTextContainer : NSTextContainer { } @end
In CircleTextContainer.m muss dann zunächst mitgeteilt werden, dass wir es nicht mehr mit einem einfachen Rechteck-Textcontainer zu tun haben. Hierzu überschreiben wir die Methode -isSimpleRectangularTextContainer: @implementation CircleTextContainer - (BOOL)isSimpleRectangularTextContainer { return NO; } …
534
Kapitel 6
Textsystem
Nun kommen wir aber zur zentralen Methode, die das eigentliche Berechnen der beschreibbaren Fläche vornimmt. Hierzu wird vom Typesetter eine Methode -line FragmentRectForProposedRect:sweepDirection:movementDirection:remaining Rect: in unserem Textcontainer aufgerufen, der aus dem vorgeschlagenen Rechteck für die nächste Zeile als Eingabewert ein mögliches Rechteck für die Zeile liefert. Außerdem wird ein Rechteck zurückgegeben, in dem weitergeschrieben werden kann, der Verschnitt sozusagen. Klarer wird dies am Code: @implementation CircleTextContainer - (NSRect)lineFragmentRectForProposedRect:(NSRect)proposedRect sweepDirection:(NSLineSweepDirection)sweep movementDirection:(NSLineMovementDirection)movement remainingRect:(NSRect*)remainingRect { …
Wie Sie an der Parameterliste erkennen können, wird zum »erhofften« Zeilenrechteck proposedRect angegeben, in welcher Richtung die Zeilen im Text laufen (sweep) und in welcher Richtung die Zeichen in der Zeile laufen (movement). Wir werden beides nicht auswerten und stattdessen von der in Deutschland üblichen Richtung von oben nach unten bzw. von links nach rechts ausgehen. … proposedRect = [super lineFragmentRectForProposedRect:proposedRect sweepDirection:sweep movementDirection:movement remainingRect:remainingRect]; …
Es sollte zunächst die Basisimplementierung aufgerufen werden, falls diese bereits Änderungen vornimmt, insbesondere einen Rand setzt. … NSTextView* view = self.textView; if( ![view isKindOfClass:[CircleTextView class]] ) { return proposedRect; } NSRect circleRect = [((CircleTextView*)view) circleRect]; …
535
SmartBooks
Objective-C und Cocoa – Band 2
Hier wird das Rechteck des Kreises vom View abgeholt. Es wäre natürlich eleganter, ein Protokoll dafür zu definieren und auf entsprechende Methoden zu prüfen, als einfach die Klasse abzufragen. Das können Sie aber schon, und wir wollen hier den Code einfach halten.
Wenn der Kreis keinen Einfluss hat, kann das Rechteck der Superimplementierung zurückgegeben werden.
Es ist ganz dienlich, verschiedene Fälle auseinander zu halten. Zunächst kann es sein, dass die Zeile gänzlich oberhalb des Kreises liegt. In diesem Falle beeinflusst der Kreis nicht das Layout, und das Zeilenrechteck kann unverändert zurückgegeben werden: … // Oberhalb des Kreises: Nichts zu tun if( NSMaxY( proposedRect ) < NSMinY( circleRect ) ) { *remainingRect = NSZeroRect; return proposedRect; } if( NSMinY( proposedRect ) > NSMaxY( circleRect ) ) { *remainingRect = NSZeroRect; return proposedRect; } …
Beachten Sie bitte, dass in diesem Falle das remainingRect leer sein muss, um das Ende der Zeile anzuzeigen.
536
Kapitel 6
Textsystem
Der Kreis zerlegt das vorgeschlagene Rechteck in ein gültiges und einen Rest.
Kommen wir zum nächsten Fall, dass das vorgeschlagene Rechteck durch den Kreis zerschnitten wird (mittlerer Bereich). In diesem Falle ist zunächst der linke Teil als gültiges Rechteck zurückzuliefern und ein Restrechteck über die Parameterliste zu liefern. Wir machen es uns in der ersten Näherung einfach und gehen von einem wirklichen Rechteck anstelle eines Kreises aus. … // Hier: Rechteck liegt in Höhe des Kreises CGFloat width; // Es beginnt links davon:Dann müssen wir es beschneiden. if( NSMinX( proposedRect ) < NSMinX( circleRect ) ) { // Das remaining liegt dann rechts vom Kreis, ... width = NSMaxX( proposedRect ) - NSMaxX( circleRect );
// ... wenn ueberhaupt if( width origin.x = NSMinX( circleRect ); remainingRect->size.width = width; } // Das proposed bis zum Kreis begrenzen. proposedRect.size.width = NSMinX( circleRect) - NSMinX( proposedRect );
…
Hier können Sie sehen, dass zunächst die Breite des Remaining-Rects ausgerechnet wird, indem einfach die rechts liegenden Kanten subtrahiert werden. Ist das Ergeb537
SmartBooks
Objective-C und Cocoa – Band 2
nis kleiner 0, so bedeutet dies, dass das vorgeschlagene Rechteck vor der rechten Seite des Kreises aufhörte, warum auch immer. In diesem Falle wird der Scan der Zeile wiederum beendet, indem NSZeroRect als Remaining-Rect gesetzt wird. Andernfalls wird das Remaining-Rect entsprechend gesetzt und das ProposedRect in seiner Breite beschnitten.
POWER Wir fummeln in unserer Implementierung lediglich in der Breite herum. Es ist durchaus auch möglich, das Rechteck nach oben und unten zu verschieben.
Das Remaing-Rect wird uns erneut zugespielt.
Kommen wir zum letzten Fall: Der Typesetter kann nicht einfach davon ausgehen, dass das Remaining-Rect, also der Rest der Zeile, uneingeschränkt verwendet werden kann. Denn es könnten ja weitere Überlappungen folgen, etwa in unserem Beispiel ein zweiter Kreis oder einfach eine kompliziertere Form, die mehrere Lücken in einer Textzeile erzeugt. Aus diesem Grunde wird die Methode erneut aufgerufen, wobei das Remaining-Rect nun als Proposed-Rect übergeben wird. (Bitte beachten Sie aber, dass eingangs der Methode ein Super-Call erfolgt, so dass man diese Gleichheit nicht vermuten darf.) In unserem Falle wissen wir aufgrund des If aus dem letzten Block, dass jedenfalls das Proposed-Rect nicht links vom Kreis beginnt, Wir können es daher auf den rechten Rand beschneiden: … } else { *remainingRect = NSZeroRect; width = NSMaxX( proposedRect ) - NSMaxX( circleRect ); if( width < 0.0 ) {
538
Kapitel 6
Textsystem
return NSZeroRect; } else { proposedRect.origin.x = NSMaxX( circleRect ); proposedRect.size.width = width; } } return proposedRect; }
Sie können das übrigens beobachten, indem Sie einmal am Anfang der Methode die y-Komponente des Zeilenrechteckes quasi als Zeilennummer loggen. Sie werden für die zerstückelten Zeilen sehen, dass die Methode doppelt ausgeführt wird. Wir müssen aber noch im Application-Delegate anstelle eines Standard-Textcontainers jetzt unseren abgeleiteten in den Stapel einfügen, bei der Stapelerzeugung also die entsprechende Zeile ändern: … #import "CircleTextView.h" #import "CircleTextContainer.h" … - (void)applicationDidFinishLaunching:(NSNotification*)notif { … // Stufe 3: Text-Container, der als Fläche ein Rechteck // in der Größe des Views hat NSRect frame = placeholder.frame; CircleTextContainer* container = [[[CircleTextContainer alloc] initWithContainerSize:frame.size] autorelease]; … }
Hiernach können Sie das Programm starten und sehen, dass wir der Sache doch schon deutlich näher kommen:
539
SmartBooks
Objective-C und Cocoa – Band 2
Für jedes Fragment wird automatisch ein Vor- und Nachlauf erzeugt.
HILFE Sie können das Projekt in diesem Zustand als Projekt »LayoutStack 2« von der Webseite herunterladen. Betrachten Sie jedoch einmal die Fragmente hinter dem Kreis: Eigentlich müssten die Buchstaben in der Mitte ja die Kreislinie berühren. Dass sie das nicht tun, liegt an dem sogenannten Line-Fragment-Padding, welches beim Textcontainer oder Typesetter gesetzt werden kann. Sie können dies nach der Erzeugung des Textcontainers mal testweise machen: - (void)applicationDidFinishLaunching:(NSNotification*)notif { … // Stufe 3: Text-Container, der als Fläche ein Rechteck // in der Größe des Views hat NSRect frame = placeholder.frame; CircleTextContainer* container = [[[CircleTextContainer alloc] initWithContainerSize:frame.size] autorelease]; [container setLineFragmentPadding:0.0]; … }
Übrigens wirkt dieser Rand nur entlang einer Zeile, weil er ja die Line-Fragmente betrifft. Ersichtlich wird dies, wenn Sie ebenso testweise einen recht großen negati540
Kapitel 6
Textsystem
ven Wert (-50.0) bestimmen: Zwar rücken die Fragmente horizontal näher aneinander, es erfolgt jedoch keine Änderung in vertikaler Richtung. Aber wir sprachen noch von der Verfeinerung. Wie Sie insbesondere an den Zeilen hinterm dem Kreis erkennen können, stanzen wir ja ein Rechteck aus. Nähern wir uns ihm genauer an. Dazu muss man etwas Trigonometrie beherrschen:
Beachten Sie, dass ein Rechteck den Kreis an ein oder zwei Stellen schneidet.
Wir müssen den Schnittpunkt des Rechteckes mit dem Kreis berechnen. Dabei ist jedoch zu bedenken, dass je nach Lage die obere, die untere oder beide Kanten die Kreislinie schneiden können. Der vom Mittelpunkt entferntere Wert ist für uns interessant. Aber noch schlimmer: Bei einer mittig liegenden Zeile könnte sogar die größte «Ausbeulung» des Kreises zwischen den Zeilen liegen. Aus diesem Grund berechnen wir die jeweiligen Winkel an den Schnittpunkten mittels der Funktion acos() (Arcus-Cosinus). Je nach Lage der Winkel wird dann über den Sinus die relevante Entfernung ermittelt. - (NSRect)lineFragmentRectForProposedRect:(NSRect)proposedRect sweepDirection:(NSLineSweepDirection)sweep movementDirection:(NSLineMovementDirection)movement remainingRect:(NSRect*)remainingRect { … // Hier: Rechteck liegt in Höhe des Kreises CGFloat width; // Es beginnt links: Dann müssen wir es rechts beschneiden. if( NSMinX( proposedRect ) < NSMinX( circleRect ) ) { // Phasenwinkel an den Schnittpunkten: NSPoint circleCenter = NSMakePoint( NSMidX( circleRect ), NSMidY( circleRect )); CGFloat radius = NSWidth( circleRect ) / 2;
541
SmartBooks
Objective-C und Cocoa – Band 2
CGFloat upperAngle = acos((NSMinY(proposedRect) - circleCenter.y) / radius); CGFloat lowerAngle = acos((NSMaxY(proposedRect) - circleCenter.y) / radius); …
Hier werden geometrische Berechnungen vorgenommen, indem die beiden Schnittwinkel ermittelt werden. … // Einordnen als ueber dem Horizont oder darunter BOOL upperAbove = YES; if( !isnan( upperAngle ) ) { upperAbove = upperAngle > M_PI_2; } …
Das ist etwas komplizierter zu verstehen: Es ist möglich, dass der obere Kreisbogen nur die untere Begrenzung des Zeilenrechteckes schneidet, es also keinen Schnittpunkt für die obere Begrenzung gibt. In diesem Falle wird für den Winkel NAN (Not a Number, kein gültiger Wert) geliefert. Hieran kann also erkannt werden, dass die Zeile oberhalb liegen muss. Nur wenn der Winkel einen gültigen Wert hat, wird nachgeschaut, wo er tatsächlich liegt. … BOOL lowerAbove = NO; if( !isnan( lowerAngle ) ) { lowerAbove = lowerAngle > M_PI_2; } …
Dies ist genau der umgekehrte Fall für eine vom Kreis nur teilweise bedeckte Zeile am unteren Rand des Kreises. … // Auswahl des Schnittpunktes: CGFloat distance; if( upperAbove && lowerAbove ) { distance = sin( lowerAngle ) * radius; …
542
Kapitel 6
Textsystem
Das oben ist der erste Fall für die Schnittpunktlage: Beide Schnittpunkte befinden sich oberhalb des Kreismittelpunktes. Dann begrenzen wir das Rechteck an der Stelle des unteren Schnittpunktes. … } else if( !upperAbove || !lowerAbove ) { distance = sin( upperAngle ) * radius; …
Zweiter Fall: Beide Schnittpunkte befinden sich unterhalb des Kreismittelpunktes. Dann begrenzen wir das Rechteck an der Stelle des oberen Schnittpunktes. … } else { distance = radius; } …
Letzter Fall: Der Mittelpunkt des Kreises befindet sich in der aktuellen Zeile. Hier wird der Radius als Maßstab herangezogen. … CGFloat width = NSMaxX( proposedRect ) - circleCenter.x + distance; proposedRect.size.width = circleCenter.x - distance - NSMinX( proposedRect ); // ... wenn ueberhaupt if( width origin.x = (circleCenter.x + distance); remainingRect->size.width = width; } …
Hier werden die Rechtecke aus den ermittelten Werten zusammengeschnitten. … } else { *remainingRect = NSZeroRect;
543
SmartBooks
Objective-C und Cocoa – Band 2
width = NSMaxX( proposedRect ) - NSMaxX( circleRect ); if( width < 0.0 ) { return NSZeroRect; } } …
Vorsicht, verdeckte Änderung: In diesen Else-Zweig gelangen wir, wenn das gewünschte Rechteck nicht vor dem Kreis liegt, der Sache nach also, wenn wir uns das zweite Mal in der Methode befinden. Jetzt müsste eigentlich dieses Rechteck noch einmal an der rechten Seite des Kreises geclippt werden. Da dies aber eine stumpfe Wiederholung der vorstehenden Trigonometrie mit anderen Vorzeichen wäre, sparen wir uns das hier. Es fehlt daher der Else-Zweig. Im Ergebnis geben wir also einfach das gewünschte Rechteck zurück, weil es ja das Remaining-Rect aus dem letzten Aufruf sein sollte. Wirklich sauber ist das nicht. Sie können freilich erneut die entsprechenden Prüfungen vornehmen, wenn Sie wollen. … return proposedRect; }
Wenn Sie jetzt dass Programm starten, sehen Sie den entsprechenden Satz. Besonders am rechten Rand des Kreises lässt sich gut erkennen, wie der Text um ihn fließt. (Links haben wir einen Flattersatz, so dass die Entfernung vom Kreis auch vom Text abhängt. Zum Testen können Sie das Textview aber auch einmal mit Punkten füllen.)
HILFE Sie können dieses Projekt als Projekt »LayoutStack 3« von der Webseite herunterladen.
Textstorage In den beiden vorangegangenen Abschnitten haben wir uns mit dem Textsatz, also der View-Ebene beschäftigt. Einen Blick wollen wir noch in den Textspeicher, also die Modelebene werfen. Wie bereits erwähnt wird das Model in einem Textstapel üblicherweise von einer Instanz der Klasse NSTextStorage gebildet, die NSMutableAttributedString beerbt, die wiederum NSAttributedString beerbt. 544
Kapitel 6
Textsystem
Während NSAttributedString und NSMutableAttributedString vor allem Attribute wie Absatzformate, Schriftschnitte (fett usw.) bereitstellen, sorgt NSTextStorage für die Kommunikation mit dem Layoutmanager.
Text und Attribute Es reicht daher in der Wirklichkeit häufig aus, den Textspeicher als AttributedString zu betrachten. Allerdings fällt bei der Arbeit mit den Klassen auf, dass sie wesentlich weniger mächtig sind als NSString. Da NSString nicht in der Vererbungshierarchie auftaucht, fehlen diese. Hierbei geht es etwa um Methoden wie -rangeOfString: usw., also solche, die unmittelbar auf den Text arbeiten, ohne Rücksicht auf Attribute zu nehmen. Mit diesem Problem kann man umgehen, indem man sich von dem Attributed-String mittels -string eine NSString-Instanz liefern lässt und dort entsprechende Arbeiten vornimmt. Mit den ermittelten Indexen und Bereichen kann man dann wiederum auf dem Attributed-String werkeln. Dies funktioniert deshalb, weil die Attribute nicht in den Text eingewoben werden, sondern sozusagen »darüber liegen«.
Attribute sind Auszeichnungen über Textbereichen.
Arten Diese zu Textstellen gehaltenen Attribute werden in Instanzen der Klasse �������� Dictionary gehalten. Dabei lassen sich drei Arten von Attributen unterscheiden:
•
Font-Attribute können an beliebiger Stelle auftreten und beschreiben Dinge wie die verwendete Schrift, Unterstreichungen usw.
•
Ruler-Attribute beziehen sich auf einen Absatz. Derzeit ist nur ein Attribut Paragraph-Style definiert. Der Eintrag im Dictionary ist dann vom Typen NSParagraphStyle.
•
Attachments sind in den Text eingebaute Bilder, für die man als Wert eine Instanz der Klasse NSTextAttachment erhält.
•
Dokumentattribute gehören indessen nicht hierher, sondern werden einmalig zum Dokument abgelegt.
545
SmartBooks
Objective-C und Cocoa – Band 2
Weil NSAttributedString als Klasse der Foundation nur die Infrastuktur definiert, bestimmt sie nicht selbst den Satz an möglichen Schlüsseln. Daher sind diese erst in der Kategorie NSAttributedString(AppKitAdditions) festgelegt. Wir werden auch gleich sehen, dass es möglich ist, eigene Schlüssel zu definieren. Attributbereiche Dieses Grundkonzept wollen wir uns einmal anschauen. Erzeugen Sie bitte ein neues Projekt mit dem Namen Storage, wobei Sie wiederum als Vorlage Cocoa Ap��� plication ohne Unterstützung von Core Data und Dokumenten verwenden. Im Header des Application-Delegates fügen Sie bitte ein Outlet und eine Deklaration für Actionmethode hinzu: @interface StorageAppDelegate : NSObject { NSWindow *window; IBOutlet NSTextView* textView; } @property (assign) IBOutlet NSWindow *window; - (void)dumpAttributes:(id)sender; @end
Die Methode müssen wir freilich implementieren: @implementation StorageAppDelegate @synthesize window; - (NSTextStorage*)storage { return [[textView layoutManager] textStorage]; } - (void)dumpAttributes:(id)sender { NSTextStorage* storage = [self storage]; NSArray* selections = [textView selectedRanges]; NSValue* selection = [selections objectAtIndex:0]; if( [selection rangeValue].location >= [storage length] ) { return; } …
546
Kapitel 6
Textsystem
Befindet sich der Cursor hinter dem Text? … NSDictionary* attributes = [storage attributesAtIndex:[selection rangeValue].location effectiveRange:NULL]; for( NSString* key in attributes ) { id item = [attributes objectForKey:key]; NSLog( @"%@ (%@): %@", key, NSStringFromClass( [item class] ), item ); } }
Im Prinzip geschieht hier nichts anderes, als dass das Dictionary mit den Attributen ausgegeben wird. Im Interface Builder ziehen Sie bitte ein Textview und einen Button in das Fenster. Den Button verbinden Sie mit der Actionmethode, umgekehrt das Outlet des Application-Delegates mit dem Textview. Dazu müssen Sie im Hauptfenster MainMenu.xib auf die hierarchische Ansicht wechseln und sich bis zum Textview durchhangeln. Für den abgebildeten Text (Bild: Wikipedia)
Ein Attributed-String kann Zeichen-, Absatz- und Bildattribute enthalten.
547
SmartBooks
Objective-C und Cocoa – Band 2
erhält man an der Stelle »mein« etwa >… NSParagraphStyle (NSParagraphStyle): Alignment 4, Line Spacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 0, Head Indent 0, Tail Indent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs ( … ), DefaultTabInterval 0, Blocks (null), Lists (null), Base WritingDirection -1, HyphenationFactor 0, TighteningFactor 0.05, HeaderLevel 0 >… NSColor (NSCalibratedRGBColor): NSCalibratedRGBColorSpace 1 0.10897 0.167964 1 >… NSUnderline (NSCFNumber): 1 >… NSFont (NSFont): "Helvetica-Bold 12.00 pt. P [] (0x10045d820) fobj=0x1001827c0, spc=3.33"
Beachten Sie, dass Sie die Attribute fett und kursiv vergeblich suchen werden, da dies (anders als Underline) Schriftschnitte sind, die also über einen eigenen Font modelliert werden. Bei Selektion des Bildes >… NSParagraphStyle (NSParagraphStyle): … >… NSAttachment (NSTextAttachment): "150px-1._FC_Köln.svg.png"
können Sie also erkennen, dass die Art und Anzahl der Attribute variieren. Da übrigens die Methode die Attribute für eine Stelle (nicht einen Bereich) liefert, reicht es aus, die Cursorposition zu verwenden. Es wird im Text vorwärts geschaut. Sie können daher in diesem Beispiel also auch den Cursor vor das Wort »mein« setzen oder vor die Graphik. Dies ist auch an dem Parameter effectiveRange erkennbar, der angibt, über welchen Bereich die gelieferten Attribute konstant sind. Dabei existiert jedoch keine Garantie, dass Sie sich außerhalb des Bereiches verändern. Will man den größten Bereich ermitteln, in dem die Attribute konstant bleiben – etwa weil man eine aufwändige Operation auf den Bereich durchführt –, so existiert dafür die Methode -attributes AtIndex:longestEffectiveRange:inRange: (NSAttributedString).
548
Kapitel 6
Textsystem
Innerhalb des Effective-Ranges ändern sich die Attribute garantiert nicht, an den Grenzen des Longest-Effectives-Ranges ändern sie sich garantiert.
AUFGEPASST Sie sollten ruhig ein wenig mit eigenen Texten experimentieren. Allerdings verhält es sich so, dass, wenn Sie einen Bezug zu Borussia Mönchengladbach, Bayer Leverkusen oder Schalke herstellen, die Anwendung abstürzt. Dies ist ein besonderes Qualitätsmerkmal! Mit -attributeRuns (NSTextStorage) können zudem einzelne, durch Attribute getrennte Teile erstellt werden.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Storage 1« von der Webseite herunterladen.
Attribute setzen Man kann freilich diese Attribute nicht nur auslesen, sondern auch selbst setzen. Dies funktioniert sowohl mit Standardattributen aus der App-Kit-Addition wie auch mit eigenen. Denn wie bereits erwähnt, stellt NSAttributedString nur die In frastruktur für Attributverwaltung zur Verfügung. Standardattribute Sie wissen vielleicht, dass das Italienische – von Fremdwörtern und Namen abgesehen – die Buchstaben »j«, »k«, »w«, »x« und »y« nicht verwendet. Suchen wir also aus einem Text diese Buchstaben heraus und färben sie rot ein. Im Header deklarieren Sie bitte eine neue Methode …: @interface StorageAppDelegate : NSObject { …
549
SmartBooks
Objective-C und Cocoa – Band 2
} … - (void)decoratePizza:(id)sender; @end
… die Sie dann wie folgt implementieren: - (NSTextStorage*)storage { return [[textView layoutManager] textStorage]; } - (void)decoratePizza:(id)sender { NSString* chars = @"JjKkWwXxYy"; NSCharacterSet* litereMiste = [NSCharacterSet characterSetWithCharactersInString:chars]; NSString* text = [[self storage] string]; NSRange hitRange; NSRange searchRange = NSMakeRange( 0, [text length] ); while( YES ) { hitRange = [text rangeOfCharacterFromSet:litereMiste options:0 range:searchRange]; if( hitRange.location == NSNotFound ) { break; } [[self storage] addAttribute:NSForegroundColorAttributeName value:[NSColor redColor] range:hitRange]; searchRange.location = hitRange.location + hitRange.length; searchRange.length = [text length] - searchRange.location; } }
Eigentlich sollte sich der Code selbst erklären. Beachten Sie aber bitte den bereits vorher angesprochenen Trick, sich vom Attributed-String eine »normale« StringInstanz erzeugen zu lassen, dann in dieser zu suchen und mit den Treffern wieder im Attributed-String zu arbeiten.
550
Kapitel 6
Textsystem
Im InterfaceBuilder fügen Sie bitte noch einen Button Decorate hinzu, den sie mit der neuen Actionmethode verbinden. Bitte testen Sie das Projekt aus, indem Sie die in Rede stehenden Buchstaben eingeben und Decorate klicken.
HILFE Sie können das Projekt in diesem Zustand als Projekt »Storage 2« von der Webseite herunterladen. Absatzattribute Absatzattribute gelten immer für den gesamten Absatz. Ihr Range ist entsprechend erweitert. Man erhält sie, indem man mit -rulerAttributes ein Dictionary ermittelt und hierauf den Schlüssel NSParagraphStyleAttributeName anwendet. Dies führt zu einer Instanz der Klasse NSParagraphStyle. Hiervon existiert eine veränderliche Subklasse NSMutableParagraphStyle.
Eigene Attribute Sowohl die Absatzattribute wie auch die Zeichenattribute können vom Programmierer erweitert werden. Zeichenattribute Da die Attribute einfach ein an einen Textbereich getackertes Dictionary sind, können auch eigene Attribute gesetzt werden. Dies kann für private Marker nützlich sein. Denken wir daran, dass wir nicht-italienische Buchstaben in einem italienischen Text nicht rot, sondern intern markieren wollen. Das sähe dann analog so aus: - (void)decoratePizza:(id)sender { … while( YES ) { … [[self storage] addAttribute:NSForegroundColorAttributeName value:[NSColor redColor] range:hitRange]; [[self storage] addAttribute:@"Pizza Hawaii" value:[NSNumber numberWithBool:YES] range:hitRange]; … } }
551
SmartBooks
Objective-C und Cocoa – Band 2
Wenn Sie jetzt den entsprechenden Decorate-Button drücken und die (zu Ihrer Hilfe weiterhin) rot markierte Textstelle selektieren, werden Sie bei einem Dump sehen, dass unser Attribut eingewoben ist: >… >… >… >…
NSParagraphStyle (NSParagraphStyle): … Pizza Hawaii (NSCFBoolean): 1 Storage[17248:a0f] NSFont (NSFont): … NSColor (NSCachedRGBColor): …
Absatzattribute Bei den Absatzattributen empfiehlt sich allerdings ein anderes Vorgehen, damit Sie nicht selbst dafür sorgen müssen, dass diese Absatzattribute immer mit den Absatzrändern übereinstimmen müssen. Sie können sich einfach eine Subklasse von NSMutableParagraphStyle erstellen und dort zusätzliche Eigenschaften unterbringen.
Zusammenfassung Sie haben einen Rundgang durch das sehr mächtige Textsystem von Cocoa unternommen. Sie haben dabei die Grundbegriffe für Schriften kennengelernt und auf einfache Weise Font-Unterstützung für eine Applikation erhalten. Dann wurde das Rendering von Fonts mittels Bezierpfaden genauer durchgegangen. Schließlich haben Sie einen Einblick in das Layout-System von OS X erhalten. Wir bauten uns unseren eigenen Layout-Stapel und fügten auf der View-Ebene und auf der Textcontainer-Ebene eigene Objekte ein. Textsatz ist eine alte Domäne von Apple. Sie können sich also leicht vorstellen, wie viele Mannjahre Entwicklung hierin stecken. So umfangreich sind auch die Möglichkeiten. Wir können hier nur einen Einblick vermitteln, der Sie, wenn Sie dies benötigen, dazu veranlasst, die Dokumentation durchzuarbeiten. Mit dem hier vermittelten Grundverständnis der Systeme dürfte Ihnen das allerdings nicht zu schwer fallen. Viel Spaß bei eigenen Experimenten!
552
Die Applikation und ihre Umgebung
7
In diesem Kapitel geht es um die Anwendung als Ganzes sowie um ihre Integration ins Betriebssystem. Gegenstand sind also solch profane Dinge wie das Dateisystem, aber auch Mac-Spezifisches: etwa Spotlight, Dock und Apple Scripting.
SmartBooks
Objective-C und Cocoa – Band 2
Das Dateisystem Sie haben bereits in Band 1 gelernt, wie man Daten speichert und wieder lädt. Dabei kam das Coding-Protokoll zum Einsatz. Manchmal ist es aber erforderlich, das Dateisystem unmittelbar anzusprechen, sei es, um Dateioperationen durchzuführen, sei es, um über Änderungen des Dateisystems informiert zu werden.
Grundlagen Um ein paar Dinge zu verstehen, sollte man sich Gedanken über die Struktur des Dateisystems machen. Wir gehen dabei vom aktuellen HFS+ aus, dem Haus-undHof-Dateisystem von OS X. Legen Sie sich bitte zu Experimentierzwecken zunächst in Ihrem Heimverzeichnis ein Unterverzeichnis Testordner an. Für die folgenden Arbeiten verwenden wir das Terminal. Hierbei handelt es sich um eine Anwendung, die es ermöglicht, dass Programme mit nur einer textuellen Benutzerschnittstelle benutzt werden können. Standardmäßig läuft die sogenannte Bash in dem Terminal. Dies ist wiederum ein Programm, mit dem Sie unter anderem Dateioperationen durch Befehle ausführen können, die per Text eingegeben werden – sozusagen die Tippalternative zum Finder. Wiederum durch die Bash können weitere Programme gestartet werden. Sie werden sehen, dass man in der Bash deutlich mehr Informationen erhält, als es mit dem Finder möglich ist. Diese Informationen benötigen wir gleich. Starten Sie die Terminal-Applikation, indem Sie nach ihr in Spotlight suchen. Shell
Computername
Aktuelles
Verzeichnis
Angezeigte Zeichen
Angemeldeter Schreibmarke Nutzer
Das Terminal mit Bash erlaubt einen detaillierteren Blick auf das Dateisystem.
Starten Sie ein Terminal und wechseln Sie mit $ cd Testordner
in dieses Unterverzeichnis. 554
Kapitel 7
Die Applikation und ihre Umgebung
GRUNDLAGEN cd (change working directory) ist der Befehl zum Wechseln des aktuellen Ordners (working directory). Standardmäßig ist beim Öffnen eines Terminals Ihr Heimordner der aktuelle Ordner. Sie können das links in der Eingabezeile vor dem Doppelpunkt erkennen. Die Tilde ~ steht dabei für das Heimverzeichnis des aktuellen Nutzers. Wollen Sie den kompletten Pfad zu Ihrem aktuellen Order wissen, müssen Sie pwd (print working directory) eingeben. Nach dem obigen Befehl sollte dies /Users/IhrName/Testordner sein.
Low-Level: Knoten und Links Eine wichtige Eigenschaft von HFS+ – typisch für unixoide Dateisysteme – liegt darin, dass es vollständig platt ist: Es gibt keine Ebenen, Dateien liegen nicht in Verzeichnissen, Dateien haben keine Namen, und Unterverzeichnisse sind auch nicht da. Das widerspricht Ihrer Erfahrung im Finder? Ja, aber es liegt daran, dass diese Hierarchie nachträglich dem Dateisystem übergestülpt wird. Zunächst ist sie nicht da. Fangen wir von vorne an: Inode-Liste … 10250299 22212742 …
Eine Datei ist (belegter) Raum in einem Dateisystem mit einer eindeutigen Nummer.
Eine Datei ist erst einmal eine Anordnung belegten Speicherplatzes auf dem Medium. Man kann in diesem Speicherplatz Daten ablegen oder von ihm lesen. Da es mehr als eine Datei gibt, müssen einzelne Dateien identifiziert werden können. Dies funktioniert auf Systemebene gerade nicht über den Pfad. Vielmehr existiert für jede Datei ein sogenannter Inode (I-Knoten, I-Node, index node). Und ähnlich den IDs unserer Instanzen hat auch der Inode eine ID, nämlich die Inode-Nummer, eine simple Zahl. Dies bedeutet zunächst einmal, dass sich sämtliche Dateien eines Dateisystems namenlos in einer flachen Liste befinden. Das meinten wir eingangs. Sie können sich übrigens die Inode-Nummern zu Dateien anzeigen lassen. Hierzu legen wir zunächst eine Datei im Testordner an: $ echo "Testtext" > testdatei.txt
555
SmartBooks
Objective-C und Cocoa – Band 2
GRUNDLAGEN Der Befehl echo gibt einfach den dahinter stehenden Text aus. Wenn Sie also echo "Testtext " eingeben, würde dieser auf dem Bildschirm erscheinen. Durch das angehängte > testdatei.txt wird diese Ausgabe umgelenkt und erscheint nicht auf dem Bildschirm, sondern in der angegebenen Datei. Da diese noch nicht existiert, wird sie erzeugt. Wir haben jetzt also eine Datei testdatei.txt mit dem Inhalt Testtext. Die neue Datei und deren Inode-Nummer können wir uns mit $ ls -i -1 10250299 testdatei.txt
anzeigen lassen. Die Zahl vor dem Dateinamen testdatei.txt ist eben die pro Dateisystem eindeutige Nummer. Sie wird freilich bei Ihnen anders lauten. Testordner Testdatei.txt …
10250299 »Testtext«
Die Datei selbst hat keinen Namen, sondern wird über ihre Inode-Nummer referenziert.
Der Befehl ls (list segments) zeigt die im Verzeichnis enthaltenen Dateien an. Wir werden noch sehen, dass dabei einige unterschlagen werden. Mit der Option -i wird auch die Inode-Nummer angezeigt. -1 sorgt dafür, dass die Ausgabe einspaltig erscheint. Sie können die Optionen auch ohne Leerzeichen und Bindestrich gleich hintereinander hängen: -i1. Wir verwenden die längere Notation, weil diese leichter zu lesen ist. Gleiche Inode-Nummern bedeuten nicht zwingend gleiche Dateien. Sie haben eine Festplatte mit zahlreichen Dateien, die alle eine Nummer haben. Ihr bester Freund kommt mit einem USB-Stick vorbei, auf dem sich ebenfalls Dateien befinden. Es kann theoretisch sein, dass zufällig eine Datei auf dem USB-Stick dieselbe Nummer abbekam wie eine Datei auf Ihrer Festplatte. Niemand konnte schließlich wissen, dass sich der Stick mit Ihrer Festplatte mal einen Computer teilen wird. Gelindert wird diese Kollision dadurch, dass jedes Dateisystem ebenfalls eine Nummer bekommt. Aber auch diese Nummer wird nicht zentral vergeben, sondern kann ganz, ganz unglücklich sich die Nummer des Dateisystems auf Ihrer Festplatte teilen. Man geht davon aus, dass dies nicht passiert.
556
Kapitel 7
Die Applikation und ihre Umgebung
Verzeichnisdateien Wir sprachen davon, dass zunächst nur eine (also platte) Liste von Dateien existiert. Gleichzeitig haben wir aber schon (hierarchische) Ordner verwendet. Irgendwie fehlt da ein Bindeglied … Zuordnung Dateiname auf Inodes Es wäre einfach ziemlich unpraktisch, wenn sich sämtliche Dateien in einer Liste befänden und nur Nummern hätten. Deshalb gibt es besondere Dateien – wohlgemerkt immer noch Dateien! –, die sich Namen zu den Inode-Nummern merken. Dies sind Verzeichnisdateien oder kurz: Verzeichnisse oder Ordner. Ordner Test.txt Party.jpg Doc.pdf …
18271920 »Ein Text« 123719111
2167236
Ein Service des Bundesministeriums der Justiz in Zusammenarbeit mit der juris GmbH www.juris.de
Gesetz über Urheberrecht und verwandte Schutzrechte (Urheberrechtsgesetz) UrhG Ausfertigungsdatum: 09.09.1965 Vollzitat: "Urheberrechtsgesetz vom 9. September 1965 (BGBl. I S. 1273), zuletzt geändert durch das Gesetz vom 7. Dezember 2008 (BGBl. I S. 2349)" Zuletzt geändert durch G v. 7.12.2008 I 2349 Stand: Hinweis: Änderung durch Art. 83 G v. 17.12.2008 I 2586 textlich nachgewiesen, dokumentarisch noch nicht bearbeitet Fußnote Textnachweis Geltung ab: 10.10.1976 Zur Nichtanwendung d. § 52a vgl. § 137k (F ab 2003-09-10) Umsetzung der EGRL 9/96 (CELEX Nr: 396L0009) vgl. G v. 2.7.1997 I 1870 Umsetzung der EWGRL 83/93 (CELEX Nr. 393L0083) vgl. G v. 8.5.1998 I 902 Umsetzung der EGRL 55/97 (CELEX Nr. 397L0055) EWGRL 28/92 (CELEX Nr: 392L0028) vgl. G v. 1.9.2000 I 1374 Umsetzung der EGRL 29/2001 (CELEX Nr: 301L0029) vgl. G v. 10.9.2003 I 1774 iVm § 137j Umsetzung der EGRL 84/2001 (CELEX Nr: 301L0029) vgl. G v. 10.11.2006 I 2587
Inhaltsübersicht
§ § § § § §
1 2 3 4 5 6
§ § § §
7 8 9 10
§
11
Allgemeines
Teil 1 Urheberrecht Abschnitt 1 Allgemeines
Abschnitt 2 Das Werk Geschützte Werke Bearbeitungen Sammelwerke und Datenbankwerke Amtliche Werke Veröffentlichte und erschienene Werke Abschnitt 3 Der Urheber Urheber Miturheber Urheber verbundener Werke Vermutung der Urheber- oder Rechtsinhaberschaft Abschnitt 4 Inhalt des Urheberrechts Unterabschnitt 1 Allgemeines Allgemeines Unterabschnitt 2 -1-
Verzeichnisdateien mappen einen Namen auf eine Datei.
Stellen wir uns also hunderte von Dateien vor, die eben alle eine Nummer haben. In einem Verzeichnis, nehmen wir Ihr Heimverzeichnis, befinden sich 3. Dann befinden sich in diesem Heimverzeichnis eben drei Einträge, die den Dateinamen mit der Dateinummer verknüpfen. Wenn Sie also einen Dateinamen angeben, so wird automatisch in diesem Verzeichnis nachgeschaut, welche Datei dazu gehört. Oder unser Testverzeichnis: Mit dem echo wurde eben nicht nur eine neue Datei auf der Festplatte mit Inode-Nummer angelegt. Vielmehr wurde gleichzeitig im aktuellen Verzeichnis Testordner ein Eintrag erzeugt, der eben den Namen der Datei (testdatei.txt) und ihre Inode-Nummer enthält. Pfadauflösung Da auch Verzeichnisse Dateien sind, bedeutet dies, dass ein Name in einer Verzeichnisdatei mit einer Verzeichnisdatei verknüpft sein kann. Auf diese Weise entstehen Unterverzeichnisse und letztlich Pfade. Das Wurzelverzeichnis heißt dabei / (Schrägstrich). Wenn Sie $ ls -i -1 /
eingeben, sehen Sie die Dateien im Wurzelverzeichnis. In diesem befindet sich mit Users das Verzeichnis der Benutzer, darin wieder mit Ihrem Benutzernamen (bei 557
SmartBooks
Objective-C und Cocoa – Band 2
mir »Amin«) Ihr Heimatverzeichnis und darin eine Datei, sagen wir Datei.txt. Bei der Pfadauflösung werden die Dateien nacheinander gelesen. /
Users System …
2
3627 Amin Anja …
839723 Musik.mp3 testdatei Testordner …
62783 testdatei.txt …
10250299 »Testtext«
/Users/Amin/Testordner/testdatei.txt ist 2->3627->839723->62783->10250299
Neben den sichtbaren Dateien existieren normalerweise unsichtbare. Man nennt diese auch die »Punkt«- oder »Dot-Dateien«, da sie mit einem Punkt beginnen. Bereits von Unix geerbt hat OS X dabei . (ein Punkt), mit dem auf das Verzeichnis selbst gezeigt wird, und .. (zwei Punkte), die das übergeordnete Verzeichnis darstellen. /
. .. Users System …
2 3627 . .. Amin Anja …
839723 . .. Musik.mp3 Bilder Testordner …
62783 . .. testdatei.txt
OS X selbst fügt noch .DS_Store (desktop services store) hinzu. Hierin werden Einstellungen zum Aussehen des Ordners im Finder dargestellt, seit OS X 10.4 aber auch Spotlight-Daten. Der Finder blendet übrigens sämtliche Dateien aus, die mit einem Punkt beginnen. Sie können sich diese Dateien im Terminal anzeigen lassen: $ ls -i -1 -a 10250281 . 409850 .. 10250300 .DS_Store 10250299 testdatei.txt
558
Kapitel 7
Die Applikation und ihre Umgebung
Links OS X unterstützt verschiedene Arten von Links, also Verweisdateien. Hard-Links Aber ein Aspekt ist hierbei noch für Sie als Programmierer interessant: Da es ja zahlreiche Verzeichnisdateien gibt, kann theoretisch eine Datei in mehreren Verzeichnissen vorkommen. Oder in einem Verzeichnis unter verschiedenen Namen: Man fügt dem Verzeichnis einfach einen neuen Eintrag hinzu und lässt diesen auf eine Datei verweisen, deren Inode-Nummer bereits einmal in der Liste auftaucht. Jeden dieser Einträge nennt man einen »Hard-Link«. /Users/Amin
839723 Musik.mp3 testdatei.txt Testordner …
62783 testdatei.txt testdatei2.txt
10250299 »Testtext«
Auf ein und dieselbe Datei kann mehrfach verwiesen werden.
Man kann dies einfach mal probieren, wenn man erneut das Terminal bemüht. Sie können sich den Inhalt der vorhin erzeugten Datei ausgeben lassen: $ cat testdatei.txt Testtext
Als Nächstes erzeugen wir im gleichen Verzeichnis einen weiteren Eintrag für diese Datei. Wir können jetzt nicht wieder das echo verwenden. Denn dies würde eine neue Datei schreiben. Vielmehr müssen wir (nur) einen neuen Eintrag erzeugen, den wir mit der bisherigen Datei verlinken: $ ln testdatei.txt testdatei2.txt
Hiermit legen Sie also im Verzeichnis einen (genau genommen: weiteren) HardLink an. Wir haben jetzt also wie in der Abbildung zwei benannte Verweise auf eine Datei oder: zwei Namen für eine Datei. Wenn Sie erneut $ ls -i -1 10250299 testdatei.txt 10250299 testdatei2.txt
tippen, sehen Sie auch, dass die gleiche Inode-Nummer vor den beiden Dateien steht. Das funktioniert nicht nur im aktuellen Verzeichnis, sondern auch in einem anderen: 559
SmartBooks
Objective-C und Cocoa – Band 2
$ ln testdatei.txt ../testdatei.txt
Hier legen wir also einen weiteren Eintrag an, diesmal im höheren Verzeichnis (..) mit demselben Namen. Und auch hier wird wieder dieselbe Inode-Nummer ausgegeben: $ ls -i -1 .. … 10250299 testdatei.txt …
Da bleibt jedoch eine Frage: Was passiert, wenn wir das Original löschen? Richtig, Sie ahnten es schon: Der Eintrag in der Verzeichnisdatei wird gelöscht, nicht jedoch die Datei selbst. Diese wird erst gelöscht, wenn es keinen Hard-Link mehr auf die Datei gibt. Ups, hören wir da jemanden Reference-Counting rufen? Man kann sich übrigens diesen mit der Option -l (Der Kleinbuchstabe l für long format) Reference-Count anzeigen: $ ls -i -l … 517812 drwxr-xr-x+ 41 … .. 10250299 -rw-r--r-3 … testdatei.txt 10250299 -rw-r--r-3 … testdatei2.txt
Nach der Dateinummer und den Zugriffrechten sehen Sie jetzt die Anzahl der Verweise. Sie bemerken hierbei auch, dass der Wert für Verzeichnisse sehr hoch ist. Das liegt daran, dass die im Verzeichnis enthaltenen Dateien ihr Verzeichnis halten. Das ist ein echter Unterschied zum Reference-Counting in Cocoa, da dort ein solcher Rückverweis nicht zur Erhöhung des Retain-Counts führt. Nach dem Gesagten müsste sich jede Änderung an der Datei aufgrund eines Verzeichniseintrages auf die Datei des anderen Verzeichniseintrages auswirken. Es ist wie mit unseren Verweisen in Cocoa, bei denen das Zielobjekt verändert wird. Probieren geht über studieren. Wir fügen einen Text an, indem wir einen Text mit >> an die erste Datei anhängen: $ echo "und noch einer" >> testdatei.txt
Lassen wir uns beide Dateien ausgeben: $ cat testdatei.txt Texttext
560
Kapitel 7
Die Applikation und ihre Umgebung
und noch einer $ cat testdatei2.txt Testtext und noch einer
Dieses System führt zu bemerkenswerten Umständen, die Sie sich bitte für später merken:
•
Man kann aus einem Pfad durch wiederholte Auflösung der Zuweisung Name nach Nummer genau eine Dateinummer (in einem Dateisystem) bestimmen, die zu dem Pfad gehört.
•
Zu einer Datei(-nummer) können jedoch mehrere Pfade gehören. Besser: Mehrere Pfade können auf ein und dieselbe Datei verweisen.
•
Man kann aus der Dateinummer nicht mehr ermitteln, welche Pfade zu einer bestimmten Dateinummer führen. Das ist häufig traurig, wenn man sich Windows-Denke nicht abgewöhnen kann.
•
Verschiebt man eine Datei im Dateisystem, so ist dies keine Änderung der Datei, sondern eine Änderung der beteiligten Verzeichnisdateien. Dies ist wichtig für Distributed-Notifications aus dem Dateisystem.
Symbolic-Links Etwas vollständig anderes sind »Symbolic-Links«. Dies sind Dateien, die den Namen einer anderen Datei speichern. Sie verweisen also explizit auf den Pfad einer anderen Datei. /Users/Amin/Testordner
62783 testdatei.txt testdatei2.txt testdateiS.txt
10250299 »Testtext« 10250300 /Users/Amin/Testordner/testdatei.txt
Ein symbolischer Link speichert nicht die Inode-Nummer, sondern den Pfad.
Auch dies probieren wir kurz aus. Zunächst legen wir einen Symbolic-Link auf unsere Testdatei an. Hierzu dient die Option -s des ln-Kommandos: $ ln -s testdatei.txt testdateiS.txt $ cat testdateiS.txt Testtext und noch einer
561
SmartBooks
Objective-C und Cocoa – Band 2
Der Finder, das können Sie nachprüfen, zeigt einen solchen Symbolic-Link mit einem kleinen Pfeilchen im Datei-Icon an. Ein kleiner Hinweis auf den Symbolic-Link
Dies bedeutet, dass, wenn man die andere Datei löscht oder ihren Namen ändert, der Symbolic-Link das Ziel nicht mehr findet: (mv verschiebt (move) eine Datei pfadmäßig, wobei die Änderung des Namens als Änderung des Pfades auch eine Verschiebeoperation in diesem Sinne ist.) $ mv testdatei.txt _testdatei.txt $ cat _testdatei.txt Testtext und noch einer $ cat testdatei2.txt Testtext und noch einer $ cat testdateiS.txt cat: testdateiS.txt: No such file or directory /Users/Amin/Testordner ?
62783 testdatei.txt testdatei2.txt testdateiS.txt
10250299 »Testtext« 10250300 /Users/Amin/Testordner/testdatei.txt
Ändert sich der Pfad einer Datei, wird sie von einem Symbolic-Link nicht mehr gefunden.
Es kommt noch besser: Lege ich eine neue Datei mit demselben Namen an, so wird diese von dem Symbolic-Link gefunden. Dieser hat sich ja den Dateinamen gemerkt: $ echo "neue Datei" > testdatei.txt $ cat testdatei.txt neue Datei $ cat testdateiS.txt neue Datei
Für unseren Hard-Link hat sich natürlich nichts geändert: $ cat testdatei2.txt Testtext und noch einer
562
Kapitel 7
Die Applikation und ihre Umgebung
/Users/Amin/Testordner
62783 _testdatei.txt testdatei.txt testdateiS.txt
8972232 »Testtext« 12402532 »neue Datei« 4328272 /Users/Amin/Testordner/testdatei.txt
Beachten Sie die Inode-Nummer: Es wird die neue Datei gefunden.
TIPP Dokumentenbasierte Cocoa-Applikationen erzeugen beim Abspeichern eine neue Datei und löschen die alte. Dies bedeutet, dass ein Hard-Link auf das alte Dokument zeigt und ein Symbolic-Link weiterhin auf das neue Dokument verweist. Bedenken Sie, dass, wenn ein Hard-Link auf das alte Dokument existiert, eine Löschoperation dieses nicht wirklich entfernt: Durch den Hard-Link existiert ja noch eine Referenz. Diese hält das Dokument am Leben. Finder-Links (Alias) Eine dritte – und letzte – Möglichkeit, einen Verweis im Dateisystem unterzubringen, sind »Finder-Links«. Sie legen einen solchen Link im Finder an, indem Sie die Datei mit gedrückter Options- und Befehlsteste verschieben. Es erscheint dann ein kleines Pfeilchen am Drag-Icon. Wenn der Link erzeugt ist, wird er ebenso wie der Symbolic-Link mit einem kleinen Pfeilchen im Finder versehen. Der Finder nennt einen solchen Link standardmäßig einen »Alias«. Finder-Links sind jedoch keine neue Technologie, sondern nur eine Kombination der beiden bestehenden: Es werden in ihm sowohl der Dateipfad als auch die Dateinummer abgelegt. Soll dieser Verweis aufgelöst werden, so schaut das Dateisystem zunächst anhand des Symbolic-Links nach, ob sich eine Datei unter diesem Pfad befindet. Scheitert dies, so wird automatisch der Versuch unternommen, mithilfe der Inode-Nummer, also des Hard-Links, die Zieldatei zu finden. Dies hat folgendes Verhalten zur Folge:
•
Wird eine Datei, auf die der Alias zeigt, verschoben oder umbenannt, ändert sich also der Pfad, so lässt sich der Symbolic-Link nicht mehr auflösen. Die Datei wird aber unter dem neuen Pfad gefunden, weil die Inode-Nummer ja unverändert blieb.
•
Wird jedoch dann eine neue Datei mit dem ursprünglichen Pfad erzeugt, so wird bei der nächsten Auflösung diese gefunden. 563
SmartBooks
Objective-C und Cocoa – Band 2
GRUNDLAGEN Cocoa bildet seit OS X 10.6 Aliasse in der Klasse NSURL mithilfe sogenannter Bookmarks ab. Hier kann bestimmt werden, ob zunächst der SymbolicLink (Standard) oder die Inode-Nummer zur Auflösung verwendet werden soll. Auch das überprüfen wir, allerdings im Finder. Löschen Sie die neu erzeugten Dateien bis auf testdatei.txt. Von dieser legen Sie nun einen Alias im Finder an. Doppelklicken Sie auf den Alias und Sie werden sehen, dass die zuletzt erzeugte Datei mit dem Inhalt »neue Datei« in TextEdit geöffnet wird. Schließen Sie wieder, ohne etwas zu sichern. Jetzt nennen Sie bitte die Datei testdatei.txt testdatei alt.txt. Ein Doppelklick auf den Alias wird die Datei erneut öffnen. Achten Sie auch auf den Dateinamen in TextEdit: testdatei alt.txt. Zwar stimmt der Dateiname nicht mehr, also der Pfad. Aber der Alias sucht dann automatisch nach der Inode-Nummer und findet die Datei unter dem neuen Namen. Und es passiert noch etwas: Das System bemerkt, dass der Pfad fehlerhaft war, und korrigiert dies automatisch! Jetzt lautet also der Pfadteil des Alias auf »testdatei alt.txt«! Das nächste Mal sucht das System nach dieser Datei, nicht mehr nach testdatei.txt. Löschen Sie den Alias wieder und benennen Sie wieder die Datei nach testdatei.txt um. Erzeugen Sie einen neuen Alias. Dieser lautet jetzt also auf »testdatei.txt«. Nun löschen Sie die Ursprungsdatei im Terminal: $ rm testdatei.txt
Bei einem Doppelklick auf den Alias wird nichts gefunden, und eine Fehlermeldung erscheint. Klicken Sie diese einfach mit OK weg. Nun erzeugen Sie bitte im Terminal die Datei neu: $ echo "Aus neu mach alt" > testdatei.txt
Wenn Sie jetzt im Finder auf den Alias doppelklicken, wird die neue Datei genommen.
564
Kapitel 7
Die Applikation und ihre Umgebung
GRUNDLAGEN Wieso war es erforderlich, die Datei im Terminal zu löschen? Ziehen Sie diese bloß in den Papierkorb, so besteht die »Gefahr« (für unsere Experimente, das ist ja ein real erwünschtes Verhalten), dass sich der Alias automatisch aktualisiert. Denn das Löschen auf dem Desktop ist gar kein Löschen, sondern lediglich ein Verschieben der Datei in ein besonderes PapierkorbVerzeichnis. Die Inode-Nummer bleibt dabei wieder unangetastet. Bei einer Aktualisierung folgt also der Alias sozusagen der Datei bis in den Papierkorb. Der Befehl rm im Terminal löscht dagegen die Datei wirklich und gibt die Inode-Nummer frei. (Natürlich nur dann, wenn kein weiterer Verweis auf die Datei existiert: Reference-Counting.)
Verzeichnisstruktur und Mounts Anders als man es etwa von Windows mit seinen Laufwerksbuchstaben gewöhnt ist, existiert bei OS X immer ein zentrales Wurzelverzeichnis. Es ist dabei wichtig zu verstehen, dass dies nicht das Wurzelverzeichnis eines bestimmten Dateisystems (auf einem Laufwerk, einer Partition usw.) ist. Vielmehr legt OS X auf oberster Ebene ein virtuelles Dateisystem beginnend mit / an, in das auf Laufwerken gefundene Dateisysteme integriert (»gemountet«) werden. Aber diese müssen nicht unbedingt ein Laufwerk, genauer: eine Partition hierauf, darstellen. Sie werden Disk-Images kennen, mit denen Software gerne ausgeliefert wird. Auch diese Image-Dateien enthalten ein komplettes Dateisystem.
GRUNDLAGEN »Alles ist eine Datei«, lautet eines der zentralen Mottos von unixoiden Dateisystemen. Nicht nur die klassischen Dateien, sondern auch, wie bereits gesehen, Verzeichnisse, (auf Laufwerken befindliche) Dateisysteme, ja sogar Tastatur und Bildschirm. Diese Vereinheitlichung macht einiges einfach möglich. Wir haben ja vorhin mit > die Bildschirmausgabe in eine Datei umgelenkt. Dabei geschah nichts anderes, als dass die Zeichenausgabe anstatt auf eine »Bildschirmdatei« in eine »klassische« Festplattendatei umgelenkt wurde.
565
SmartBooks
Objective-C und Cocoa – Band 2
Die Struktur der Verzeichnisse ist dabei wohldefiniert. Sie finden also an bestimmten Stellen bestimmte Verzeichnisse vor: Pfad
Bedeutung
~
Verweis auf das Verzeichnis des aktuellen Nutzers
/Applications Anwendungsverzeichnis /Network
Netzwerk
/System
Systemverzeichnis
/Users
Benutzerverzeichnisse
/Volumes
Vorhandene Dateisysteme, hierzu später mehr
/Library
Bibliotheken, hierzu später mehr
/usr
Nicht etwa Benutzer (User), sondern Systemressourcen (»Unix System Resources«)
Volumes Volumes ist der standardmäßige Mount-Point für Dateisysteme. Hiermit bezeichnet man den Ort, an dem ein Dateisystem in die Struktur des Systembaumes eingebaut wird. Sie können sich dieses Verzeichnis wie jedes andere anzeigen lassen: $ ls -i -1 /Volumes 10261103 Macintosh HD 2 iDisk
Stecken Sie einen USB-Stick ein, öffnen Sie eine Disk-Image-Datei oder legen Sie eine CD ein, es passiert stets dasselbe: Das Verzeichnis/Volumes erweitert sich. Achten Sie aber auch darauf, dass dabei Inode-Nummern innerhalb der Dateisysteme doppelt belegt sein können, weil es sich ja um Dateien auf verschiedenen Dateisystemen handelt!
AUFGEPASST Sie sehen übrigens nicht alle vorhandenen Dateisysteme, sondern nur diejenigen, die dem Nutzer unmittelbar zur Verfügung stehen. Eine vollständige Auflistung erhalten Sie durch den Befehl mount im Terminal. Library In diesem Verzeichnis können Anwendungen private Verzeichnisse öffnen. Sie sollten das allerdings nicht unmittelbar in diesem Verzeichnis machen, sondern im Unterordner Application Support. Außerdem existiert dieses Verzeichnis an verschiedenen Stellen, worauf wir gleich bei den Domains eingehen. 566
Kapitel 7
Die Applikation und ihre Umgebung
Implementierung Diese Verzeichnisse sind übrigens durchaus (virtuelle) Links auf bestehende Verzeichnisse. Dies lässt sich etwa an dem Verzeichnis Users demonstrieren: Es ist ja schön, dass dieses Verzeichnis virtuell im Wurzelverzeichnis enthalten ist. Aber irgendwo müssen die Daten ja auch physikalisch gespeichert werden. Es wird einfach virtuell ein Hard-Link auf das User-Verzeichnis erzeugt. Dieses befindet sich üblicherweise auf der Systemfestplatte. Man kann das überprüfen: $ ls -i -1 / … 27413 Users … $ ls -i -1 '/Volumes/Macintosh HD' … 27413 Users …
Domains Bereits bei Library hatten wir erläutert, dass hier – genauer in Library/Application Support – Anwendungen Daten hinterlegen können. Denken Sie etwa an Vorlagendokumente oder an mitgelieferte Daten. Das war aber nur die halbe Wahrheit. Denn Sie werden vielleicht schon bemerkt haben, dass es dieses Verzeichnis an mehreren Orten gibt: /Library, /System/Library, /Network/Library und ~/Library. Dies geht einher mit den verschiedenen Domains des Dateisystems. Funktion Jede Domain hat eine bestimmte Bedeutung im Gesamtsystem. So befindet sich zum Beispiel das Verzeichnis Library in Domain Pfad
Bedeutung
System
/System
Der für das System vorgesehene Bereich
Local
/
Dateien, die sich auf den Computer beziehen
Network /Network
Dateien, die sich auf das Netzwerk beziehen
User
~
Dateien, die sich auf den jeweiligen Nutzer beziehen
Shared
/Users/Shared Dateien, die sich auf alle Nutzer des Computers beziehen
Library ist dabei nur ein Fall. Grundsätzlich können auch andere Standardverzeichnisse mehrfach vorhanden sein. So existiert etwa für Anwendungen, die nur von einem Nutzer verwendet werden, der Pfad ~/Applications anstelle von /Appli567
SmartBooks
Objective-C und Cocoa – Band 2
cations. Es ist nur in der Regel nicht sinnvoll, Anwendungen nur für einen Nutzer zu speichern. Daher ist dieser Ordner standardmäßig nicht nur leer – er existiert erst gar nicht.
Lokalisierte Pfade Ihnen ist sicher schon aufgefallen, dass ich hier immer englische Pfadnamen verwende und dass auch das Terminal diese so anzeigt, der Finder indessen deutsche. OS X erlaubt es, Ordnernamen zu lokalisieren. Dabei existieren zwei Systeme: Systempfade Systemdateien werden automatisch lokalisiert. Aus Applications wird also ohne weiteres Zutun des Nutzers Programme, aus Users wird Benutzer usw. OS X fügt zur Markierung automatisch eine Datei .localized in diese Verzeichnisse ein. .localized-Ordner
AUFGEPASST Die folgende Lokalisierung funktioniert nur dann, wenn im Finder die Einstellung Alle Suffixe anzeigen ausgeschaltet ist. Schalten Sie diese im Betrieb aus, müssen Sie sich neu einloggen. Das mache ich dann jetzt auch einmal. Bis gleich! Wenn einem Ordner die Endung .localized hinzugefügt wird, versucht das System ebenfalls, den Ordnernamen zu lokalisieren. Hierzu muss sich ein Subordner .localized in dem Verzeichnis befinden, in der sich wiederum für jede Sprache eine Stringsdatei befindet. Dies können wir einmal ausprobieren. Zunächst ändern wir den Namen unseres Testordners auf Testfolder.localized und denjenigen der Datei testdatei.txt auf testfile.txt: $ cd ~ $ mv Testordner Testfolder.localized $ ls -1 -a … Testfolder.localized $ cd Testfolder.localized $ mv testdatei.txt testfile.txt $ ls -1 -a … testfile.txt
Bitte überprüfen Sie das Ergebnis im Finder und achten Sie bitte darauf, dass die Endung .localized des Verzeichnisses im Finder nicht angezeigt wird. (Sie sollten 568
Kapitel 7
Die Applikation und ihre Umgebung
durch Hin-und-Her-Wechsel in ein anderes Verzeichnis den Finder dazu zwingen, den Ordner neu anzuzeigen.) Nun legen wir als Unterverzeichnis das Lokalisierungsverzeichnis an. Dies trägt den Namen .localized. Wechseln Sie hiernach in das Verzeichnis: $ mkdir .localized $ cd .localized
Es muss jetzt für jede Sprache eine Property-List (Band 1, S. 328 ff.) nach altem Format angelegt werden, die den Namen des Verzeichnisses übersetzt. Sie heißt nach dem Ländercode, etwa de für Deutschland, und besitzt als Endung .strings. Die Syntax ist einfach "Dateisystemname" = "lokalisierter Name"; Eine solche de.strings legen wir nun an: $ echo '"Testfolder" = "Testordner";' > de.strings
Wenn Sie im Finder das enthaltene Verzeichnis erneut anschauen (damit der Finder sich aktualisieren kann), sehen Sie nunmehr die gewohnte Bezeichnung Testordner. Dieser lokalisierte Name wird »Display-Name« genannt. Bundles Eine weitere Möglichkeit ist freilich die Lokalisierung über das Bundle, insbesondere das Applikation-Bundle. Hierzu dient in Xcode der Schlüssel bundle display name.
GRUNDLAGEN Sie sollten nicht irgendwelche Verzeichnisse lokalisieren. Das ApplicationBundle ist der wichtigste Fall und lässt sich so lokalisieren. Für die Lokalisierung von Applikaitionsnamen bietet Xcode Hilfe an.
Path-Services Cocoa bietet verschiedene Funktionalitäten an, um mit Dateipfaden umzugehen: Zunächst sei hier noch einmal an die Methoden zur Bearbeitung von Strings als Pfade erinnert (Band 1, 314 f.). Aber die Foundation enthält einige weitere Funktionen, mit denen Pfade in den vorgenannten Domains ermittelt werden können. Sie verwenden bitte stets diese Funktionen und kodieren nicht feste Pfade in Ihrer Source. Diese können nämlich jederzeit von Apple geändert werden.
569
SmartBooks
Objective-C und Cocoa – Band 2
Zentral ist die Funktion NSSearchPathForDirectoriesInDomains(), welche Standardpfade als Antwort auf die Frage nach einem bestimmten Standardverzeichnis in einer oder mehreren Domains liefert. Der erste Parameter bestimmt dabei die Domains. Als Konstanten sind dabei NSUserDomainMask, NSSystemDomainMask, NSNetworkDomainMask, NSLocalDomainMask und NSAllDomainMask definiert, die angesichts der vorstehenden Erläuterungen nicht mehr näher ausgeführt zu werden brauchen. Sie können logisch verodert werden, um so in mehreren Bereichen gleichzeitig zu suchen. Das Ergebnis ist ein Array, welches in seiner Reihenfolge durchsucht werden muss. Kann ich also mit dem ersten Eintrag etwas anfangen, muss ich diesen nehmen, sonst den zweiten ausprobieren usw. Sucht man etwa die Library in allen Domains, so erhält man entsprechend vier Treffer, die in der angegebenen Reihenfolge abgearbeitet werden sollten. Allerdings darf man keine Annahmen über die Trefferzahl tätigen. Apple behält es sich vor, die Verzeichnisstruktur zu ändern. Der zweite Parameter stellt sozusagen den Suchbegriff dar. Es existiert dabei eine Liste vordefinierter Standardverzeichnisse. Die vollständige Liste finden Sie in der Dokumentation von NSSearchPathForDirectoriesInDomains(). Hier die wichtigsten: Konstante
Beschreibung
NSApplicationSupportDirectory Verzeichnis, das der Applikation gehört NSCacheDirectory
Zwischenspeicher, die gelöscht werden dürfen
NSDocumentDirectory
Dokumente
NSDownloadsDirectory
Downloads
NSLibraryDirectory
Hilfsverzeichnisse für Applikationen
Es sei angemerkt, dass nicht jedes dieser Verzeichnisse in jeder Domain existiert. Aber so existiert tatsächlich ein Applikationsverzeichnis für einzelne Benutzer: NSArray* paths; paths = NSSearchPathForDirectoriesInDomains( NSApplicationDirectory, NSUserDomainMask, YES ); if( [paths count] > 0 ) { NSLog( @"Benutzeranwendungen: %@", [paths objectAtIndex:0] ); } // Ergebnis: /Users/Amin/Applications
570
Kapitel 7
Die Applikation und ihre Umgebung
Wichtigster Fall für eine Pfadsuche ist der Ordner Application Support. Hier darf die Anwendung wüten, wie sie will. So nutzt etwa das Template für Core-Data-Anwendung ohne Dokumente dieses Verzeichnis, um das Model zu speichern. Der Code im Template: - (NSString *)applicationSupportFolder { NSArray *paths = NSSearchPathForDirectoriesInDomains( NSApplicationSupportDirectory, NSUserDomainMask, YES ); NSString *basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : NSTemporaryDirectory(); return [basePath stringByAppendingPathComponent:@"AppName"]; }
Wie Sie der bedingten Zuweisung entnehmen können, verwendet eine solche Applikation als Speicherort das Verzeichnis für Zwischendateien, wenn kein Application-Support-Directory existiert. Neben dieser allgemeinen Funktion existieren aus Bequemlichkeitsgründen die Funktionen NSHomeDirectoy(), welche das Heimatverzeichnis des aktuellen Benutzers liefert, und obiges NSTemporaryDirectory(), welches für Zwischendateien benutzt werden kann.
Filemanager Will man »echte« Dateioperationen jenseits des Codings vornehmen, ist NSFileManager die erste Anlaufstelle. Er behandelt also im Wesen nicht Operationen bezüglich des Dateiinhaltes, sondern bezüglich des Dateisystems. Der Filemanager arbeitet grundsätzlich pfadbasiert, nicht Inode-basiert. Es existiert ein Default-Manager als Singleton, den man sich entsprechend mit +defaultManager holt. Bitte legen Sie ein neues Projekt aus dem Template Cocoa Application mit Unterstützung für Dokumente, jedoch ohne Unterstützung von Core Data an und nennen Sie es FileManager. In MyDocument.h fügen Sie bitte ein Outlet und eine Action ein
571
SmartBooks
Objective-C und Cocoa – Band 2
@interface MyDocument : NSDocument { IBOutlet NSTextField* basePathTextField; } - (IBAction)readDirectoryAtPath:(id)sender; @end
die Sie bitte in MyDocument.m zunächst leer implementieren: @implementation MyDocument - (IBAction)readDirectoryAtPath:(id)sender { }
In MyDocument.xib platzieren Sie ein Textfeld, mit dem Sie das Outlet des Dokumentes über den File’s Owner herstellen. Umgekehrt wird aus derActionmethode die Action des Textfeldes.
Verzeichnisse lesen Zunächst lassen sich mit dem Filemanager Verzeichnisse lesen. Dafür existieren drei Methoden: -subpathsAtPath: liefert sämtliche Dateien und Unterverzeichnisse an den angegebenen Ort, wobei Unterverzeichnissen gefolgt wird und auch Symbolics-Links aufgelöst werden. Die Punkt-Dateien ., .. und ._… werden nicht aufgenommen. Bundles werden als Verzeichnisse und nicht als Dateien behandelt, so dass deren Inhalt sichtbar wird (entspricht Paketinhalt zeigen im Finder). Als Ergebnis erhält man ein Array von Strings, welche die einzelnen Pfade enthalten. Es wird also nicht die Verzeichnisstruktur durch eine entsprechende Datenstruktur abgebildet. Die Strings sind relativ zu dem als Parameter übergebenen Pfad zu verstehen. Die Datei /Users/Amin/Test.txt wird also als Test.txt geliefert, wenn ab /Users/Amin gesucht wird, und als Amin/Test.txt, wenn ab /Users gesucht wird. Verzeichnisse erhalten kein angehängtes /. Da die Operation den Unterverzeichnissen folgt, kann sie recht lange dauern. Erstellen wir ein kleines Beispiel: - (IBAction)readDirectoryAtPath:(id)sender { NSFileManager* manager = [NSFileManager defaultManager];
572
Kapitel 7
Die Applikation und ihre Umgebung
// Hole Basispfad und wandele ggfls. @"~" um NSString* basePath = [basePathTextField stringValue]; basePath = [basePath stringByExpandingTildeInPath]; // Lese Verzeichnis NSArray* content = [manager subpathsAtPath:basePath]; NSLog( @"%@", content ); }
Sie können jetzt in der Anwendung einen Pfad – auch mit beginnend ~ für Ihr Heimatverzeichnis – angeben und sich das Ergebnis in der Konsole anzeigen lassen. Es ist übrigens möglich, aus dem Finder ein Verzeichnis auf das Textfeld zu ziehen. Sie sollten allerdings ein Unterverzeichnis als Ausgangspunkt nehmen, wenn Sie Ihre Festplatte nicht einem Dauertest unterziehen wollen.
HILFE Sie könenn das Projekt in diesem Zustand als »FileManager 1« von der Webseite herunterladen. -enumeratorAtPath:, NSDirectoryEnumerator Mit dieser Methode ist es möglich, sich einen besonderen Abzähler von der Klasse NSDirectoryEnumerator geben zu lassen. Es handelt sich sozusagen um die iterative Variante der vorangegangenen Methode: Jeder Pfad wird einzeln als String zurückgegeben, wobei die Notierung relativ zu dem Startpfad erfolgt. Soll also der vollständige Pfad bekannt sein, müssen diese mit einem / zusammengefügt werden: NSString* completePath = [NSString stringWithFormat:@"%@/%@", basePath, filePath];
Man kann aber unmittelbar zu der zuletzt enumerierten Datei mit -fileAttributes (NSDirectoryEnumerator) die Attribute erhalten. Dies verhindert einen zusätzlichen Zugriff mit anderen Methoden. Symbolischen Links wird nicht gefolgt, auch dann nicht, wenn sie auf Verzeichnisse zeigen und daher wiederum eine Verzeichnisstruktur aufbauen. Somit werden ».« und »..« nicht zurückgeliefert. Es ist möglich, mit -skipDescendants (NSDirectoryEnumerator) ein Verzeichnis auszulassen, wenn es nicht interessiert.
573
SmartBooks
Objective-C und Cocoa – Band 2
- (IBAction)readDirectoryAtPath:(id)sender { NSFileManager* manager = [NSFileManager defaultManager]; // Hole Basispfadund wandele ggfls. die ~ um NSString* basePath = [basePathTextField stringValue]; basePath = [basePath stringByExpandingTildeInPath]; // Hole Abzaehler NSDirectoryEnumerator* enumerator = [manager enumeratorAtPath:basePath]; // Iteriere NSString* filePath; while( (filePath = [enumerator nextObject]) != nil ) { NSLog( @"%@\n%@", filePath, [enumerator fileAttributes] ); } }
HILFE Sie könenn das Projekt in diesem Zustand als »FileManager 2« von der Webseite herunterladen. -contentsOfDirectoryAtPath:error: liefert nur den (flachen) Inhalt des angegebenen Verzeichnisses als Array von Strings. Symbolic-Links werden nicht aufgelöst und Punkt-Dateien ebenfalls nicht aufgenommen ( ., .., ._…). Es eignet sich damit für eine schrittweise Erkundung des Dateisystems, bei der Unterverzeichnisse erst gelesen werden, wenn dies – insbesondere auf eine Nutzerinteraktion hin – erforderlich wird. Auch hier ein kleines Beispiel: - (IBAction)readDirectoryAtPath:(id)sender { NSFileManager* manager = [NSFileManager defaultManager]; // Hole Basispfadund wandele ggfls. die ~ um NSString* basePath = [basePathTextField stringValue]; basePath = [basePath stringByExpandingTildeInPath];
574
Kapitel 7
Die Applikation und ihre Umgebung
// Iteriere NSError* error = nil; NSArray* items = [manager contentsOfDirectoryAtPath:basePath error:&error]; if( items == nil ) { [[NSApplication sharedApplication] presentError:error]; return; } for( id item in items) { NSLog( @"item %@", item ); } }
Der Verzeichnisinhalt wird als Array von NSString-Instanzen zurückgeliefert. Es handelt sich um Dateinamen ohne Pfadangabe.
Verzeichnis- und Dateioperationen Ebenfalls ist es möglich, Verzeichnisse anzulegen. Auch hierfür findet sich ein Beispiel in dem Template für Core-Data-Projekte ohne Dokumente: - (NSPersistentStoreCoordinator *) persistentStoreCoordinator { … NSFileManager* fileManager; NSString* appSupportFolder = nil; … fileManager = [NSFileManager defaultManager]; appSupportFolder = [self appSupportFolder]; if( ![fileManager fileExistsAtPath:appSupportFolder isDirectory:NULL] ) { [fileManager createDirectoryAtPath:appSupportFolder attributes:nil]; } …
Man kann ebenso mit Methoden wie -copyItemAtPath:toPath:error: , -moveItem AtPath:toPath:error: und -removeItemAtPath:error: Dateien kopieren, verschieben bzw. löschen. Das funktioniert freilich auch mit ganzen Verzeichnissen, weil die ja auch nur Dateien sind. Ebenso werden mit -linkItemAtPath:toPath:error: und -createSymbolicLinkAtPath:withDestinationPath:error: harte bzw. symbolische Links unterstützt. 575
SmartBooks
Objective-C und Cocoa – Band 2
Dateiattribute Wichtig ist auch die Möglichkeit, Dateiattribute zu ermitteln und zu setzen. Hierzu dienen die Methoden -attributesOfItemAtPath:error: bzw. setAttributes:ofItemAtPath:error:. Die Attribute werden in einer Instanz von NSDictionary abgelegt. Bedenken Sie, dass einige Attribute das Dateisystem behandeln, nicht einen Eintrag im Dateisystem. Last but not least lässt sich mit -displayNameAtPath: der oben erläuterte Name der Datei für den Benutzer ermitteln. Dieser lokalisierte Name kann allerdings nie für Dateioperationen verwendet werden. Er dient alleine der Benutzeroberfläche.
File-Wrappers File-Wrappers sind die objekt-orientierte Darstellung von Dateien. Im Gegensatz zum File-Manager behandeln sie auch den Inhalt einer Datei. Dafür existieren nur sehr eingeschränkt Operationen zu der Stellung der Datei im Dateisystem. Faustregel: Der File-Wrapper bezieht sich auf den Inhalt einer Datei, der File-Manager auf die Stellung der Datei im Dateisystem. Instanzen der Klasse NSFileWrapper können aus einer bestehenden Datei mittels Angabe einer URL erzeugt werden (lesender Zugriff) oder mit Hilfe einer Instanz von NSData, die den Inhalt der Datei trägt und noch abgespeichert werden muss (schreibender Zugriff). Handelt es sich bei der Datei um ein Verzeichnis, so können File-Wrapper für die enthaltenen Dateien abgeholt werden. Wichtig ist im Zusammenhang mit dem File-Wrapper, dass er sich die Datei anhand der Inode-Nummer merkt. Das entspricht eben seiner Aufgabe. Hieraus resultiert, dass ein File-Wrapper zwar weiß, wie er entstanden ist. Diese Pfadangabe ist aber nicht verlässlich, weil sich der Pfad ändern kann. Und aus der Inode-Nummer lässt sich eben nicht mehr der Pfad eindeutig ermitteln. Man sollte also tunlichst wenig mit dieser Angabe arbeiten. Außerdem kennt der File-Wrapper den Preferred-Filename, der von dem Dateinamen abweichen kann, wenn zwei gleiche Namen in einem Verzeichnis konkurrieren. File-Wrappers können normale Dateien, Verzeichnisdateien und Links sein.
576
Kapitel 7
Die Applikation und ihre Umgebung
File-Handles und Sockets Jedes Mal, wenn unter Mac OS X eine Datei geöffnet wird, wobei es auch das Öffnen zum Schreiben gibt, wird der Datei eine Dateinummer gegeben. Diese bezeichnet man auch als File-Handle. In Cocoa ist dies als NSFileHandle modelliert. File-Handles behandeln allerdings eine Datei im Sinne von Unix als eine Möglichkeit, Daten zu senden bzw. zu empfangen. Sie sind also zum einen komplett inhaltsbezogen. Zum anderen müssen File-Handles sich nicht auf eine Datei, die sich in einem Dateisystem befindet, beziehen. Insbesondere können Kommunikationskanäle zwischen Prozessen bezeichnet werden. Dann öffnet einer diesen Kanal zum Lesen und der andere zum Schreiben. Dies ist im Kapitel über Nebenläufigkeit noch von Bedeutung. Das Lesen von Dateien stellt sich daher nicht zwingend als einseitige Operation dar, mit der Daten in eine Datei geschrieben oder von einer Datei gelesen werden. Vielmehr müssen Sie davon ausgehen, dass auf der anderen Seite einer Datei jemand sitzt, der die Leitung befüllt. Dies ist etwa nützlich, wenn wir im Kapitel über Nebenkäufigkeit Hintergrundprozesse starten. Hier wollen wir eine kleine Einführung geben:
Dateien synchron lesen Gleichermaßen als Fingerübung wollen wir zunächst eine große Datei lesen. Zuallerst kopieren Sie eine solche Datei – 100 MB sollten es schon sein – auf Ihren Desktop und nennen diese big (ohne Endung). Erzeugen Sie sich ein neues Projekt FileHandle ohne Unterstützung von Dokumenten und Core Data (Vorlage: Cocoa Application). Machen Sie im Header eine Actionmethode bekannt @interface FileHandleAppDelegate : NSObject< NSApplicationDelegate > { NSWindow *window; NSMutableData* data; } @property( assign ) IBOutlet NSWindow *window; @property( retain ) NSMutableData* data; - (IBAction)readFile:(id)sender; @end
und implementieren Sie diese: @implementation FileHandleAppDelegate @synthesize window; @synthesize data;
577
SmartBooks
Objective-C und Cocoa – Band 2
- (IBAction)readFile:(id)sender { NSString* path = [@"~/Desktop/big" stringByExpandingTildeInPath]; NSFileManager* manager = [NSFileManager defaultManager]; NSDictionary* attribs = [[manager attributesOfItemAtPath:path error:NULL]; NSLog( @"Attribs: %@", attribs ); // Starte lesen NSFileHandle* fileHandle = [NSFileHandle fileHandleForReadingAtPath:path]; NSData* rawData = [fileHandle readDataToEndOfFile]; self.data = [NSMutableData dataWithData:rawData]; NSLog( @"Datei gelesen" ); } … - (id)init { self= [super init]; if( self ){ self.data = [NSMutableData data]; } return self; } - (void)dealloc { self.data = nil; [super dealloc]; } @end
Im Interface Builder ziehen Sie einen Button in das Fenster und verbinden diesen mit der neuen Actionmethode. Sie könnnen das Programm jetzt starten und mit dem Button die Daten lesen. Wenn Sie die Datei hinreichend groß gewählt haben, dann dauert es einige Sekun578
Kapitel 7
Die Applikation und ihre Umgebung
den, bis sie gelesen ist. Da die Methode wartet (blockiert), bis die Datei gelesen ist, haben Sie auch keine Möglichkeit, einen bestimmten Progress-Indicator zu starten und zu aktualisieren.
HILFE Sie können das Projekt in diesem Zustand als Projekt »FileHandle 1« von der Webseite herunterladen.
Dateien asynchron lesen Deshalb versuchen wir jetzt, die Datei »im Hintergrund« zu lesen, dies bedeutet parallel zu unserem eigentlichen Programm. Dazu sagen wir dem File-Handle, dass er lediglich auf Daten warten soll und bei neuen Daten diese entgegennimmt. Um das zu visualisieren, fügen Sie bitte der Klasse ein Outlet hinzu: @interface FileHandleAppDelegate : NSObject< NSApplicationDelegate > { IBOutlet NSProgressIndicator* readProgressView; … }
und dem Nib eine entsprechende Progress-Bar, die Sie auf determinated setzen. In der Implementierung bauen wir dann die Leseroutine um: - (void)receiveData:(NSNotification*)notification { NSDictionary* info = [notification userInfo]; NSData* newData = [info valueForKey:@"NSFileHandleNotificationDataItem"]; if( [newData length] == 0 ) { NSLog( @"Ende der Verbindung" ); return; } [readProgressView incrementBy:[newData length]]; [[self data] appendData:data]; [[notification object] readInBackgroundAndNotify]; } - (IBAction)readFile:(id)sender { NSString* path = [@"~/Desktop/big" stringByExpandingTildeInPath];
579
SmartBooks
Objective-C und Cocoa – Band 2
NSFileManager* manager = [NSFileManager defaultManager]; NSDictionary* attribs = [manager fileAttributesAtPath:path traverseLink:NO]; long size = [[attribs objectForKey:NSFileSize] longValue]; [readProgressView setMaxValue:size]; [readProgressView setDoubleValue:0.0]; // Starte lesen NSFileHandle* fileHandle = [NSFileHandle fileHandleForReadingAtPath:path]; NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector( receiveData: ) name:NSFileHandleReadCompletionNotification object:fileHandle]; [fileHandle readInBackgroundAndNotify]; }
Der Trick besteht darin, dass nunmehr mit -readInBackgroundAndNotify (NS FileHandle) das Lesen der Datei in den Hintergrund verschoben wird. Sind neue Daten verfügbar, so wird eine Notification ausgelöst, die wir observieren. In der entsprechenden Behandlungsroutine werden dann die neuen Daten geholt. Beachten Sie bitte hier, dass am Ende der Methode erneut das Lesen im Hintergrund aktiviert werden muss. (Das Objekt der Notification ist unser File-Handle.)
HILFE Sie können das Projekt in diesem Zustand als Projekt »FileHandle 2« von der Webseite herunterladen. Sie werden bemerken, dass sich die Performance des Filelesens dramatisch verschlechtert. Hierbei ist aber zu bedenken, dass dies zum Teil auf die Struktur unserer Applikation zurück geht: Wir lesen die Datei von einem vergleichsweise schnellen Medium und machen sonst nichts. Daher mussten wir zur Erzielung des Effektes eine sehr große Datei nehmen. In der Realität der Softwareentwicklung werden Sie aber häufig mit diesen Daten etwas machen wollen, so dass sich bereits eine merkliche Verzögerung für den Nutzer bei kleineren Daten einstellt. Dann fällt der Overhead des Hintergrundprozesses nicht mehr so ins Gewicht. Mit ande-
580
Kapitel 7
Die Applikation und ihre Umgebung
ren Worten: In unserem Beispiel besitzt der Overhead für den Hintergrundprozess das schlechteste Verhältnis. Dennoch: Wir sind durchaus Gegner der Parallelisierung, wenn die Benutzeroberfläche dann ohnehin gesperrt ist. Der Nutzer hat ja nichts davon. (Ein Vorteil liegt allerdings darin, dass die Applikation für OS X weiter lebt, also nicht der Beachball erscheint, wenn die Applikation längere Zeit Daten lädt.)
GRUNDLAGEN Wenn hier wie in der Dokumentation von Apple von Prozessen die Rede ist, so ist das untechnisch zu verstehen. Die Hintergrundtätigkeit wird nicht über Threads oder Prozesse gestartet, sondern läuft als Ereignis über die Run-Loop. Dies hat den Vorteil, dass man sich keine Gedanken über die Zugriffskontrolle machen muss. Welche Schwierigkeiten sich nämlich insgesamt aus Threads ergeben, lernen Sie in dem entsprechenden Kapitel.
Workspace Der Workspace bildet die eigentliche Systemumgebung in Cocoa nach. Dementsprechend sind hier Dinge abzufragen, die das System betreffen, wobei der Systembrowser Finder hier ebenfalls als Bestandteil des System angesehen werden kann:
• • • •
Notification-Center für Systemnachrichten. Methoden um Dateioperationen vorzunehmen Verwaltung von Dateitypen Start von Applikationen
Notification-Center Sie kennen aus dem ersten Band bereits das (lokale) Notification-Center für Benachrichtigungen innerhalb eines Programmes und das Distributed-NotificationCenter für Nachrichten zwischen Programmen. Das Workspace-Notification-Center unterrichtet über Systemänderungen. Wir können dies anhand einer kleinen Anwendung ausprobieren: Legen Sie ein neues Projekt Workspace ohne Unterstützung von Core Data und Dokumenten an. Wir erzeugen uns eine Klasse EventDescription von der Vorlage Objective-C class, Subclass NSObject, welche eine einzelne Notification aufnimmt: 581
SmartBooks
Objective-C und Cocoa – Band 2
@interface EventDescription : NSObject { NSDate* date; NSString* name; NSDictionary* parameters; } @property( copy ) NSDate* date; @property( copy ) NSString* name; @property( copy ) NSDictionary* parameters; - (id)initWithDate:(NSDate*)initDate name:(NSString*)initName parameters:(NSDictionary*)initParameters; @end
Die dazugehörige Implementierung: @implementation EventDescription @synthesize date, name, parameters; - (id)initWithDate:(NSDate*)initDate name:(NSString*)initName parameters:(NSDictionary*)initParameters { self = [super init]; if( self ) { self.date = initDate; self.name = initName; self.parameters = initParameters; } return self; } - (void) dealloc { self.date = nil; self.name = nil; self.parameters = nil; [super dealloc]; } @end
582
Kapitel 7
Die Applikation und ihre Umgebung
Sie sehen, dass es sich um nichts Besonderes handelt, eine übliche Modelklasse, die ein paar Eigenschaften speichert. Im Application-Delegate erzeugen wir uns einen Anker für die einzelnen Instanzen von EventDescription: @interface WorkspaceAppDelegate : NSObject { NSWindow *window; NSArray* eventDescriptions; } @property( assign ) IBOutlet NSWindow *window; @property( copy ) NSArray* eventDescriptions; @end
Wirklich interessant ist die Implementierung des Application-Delegates: #import "EventDescription.h" @implementation WorkspaceAppDelegate @synthesize window; @synthesize eventDescriptions; - (void)catcherMethod:(NSNotification*)notification { NSString* name = [notification name]; if( [name hasPrefix:@"NS"] ) { name = [name substringFromIndex:2]; } NSMutableDictionary* parameters = [NSMutableDictionary dictionary]; NSDictionary* info = [notification userInfo]; for( NSString* key in info ) { NSString* value = [[info objectForKey:key] description]; [parameters setObject:value forKey:key]; }
583
SmartBooks
Objective-C und Cocoa – Band 2
EventDescription* event = [[[EventDescription alloc] initWithDate:[NSDate date] name:name parameters:parameters] autorelease]; [[self mutableArrayValueForKey:@"eventDescriptions"] addObject:event]; } …
Diese Methode wird aufgerufen, wenn eine Notification empfangen wird. Sie macht im Wesentlichen nichts anderes, als die Werte auf der Notification in eine Instanz der Klasse EventDescriptor zu kopieren. Etwaige weitere Parameter bekommt die Methode in einem Info-Dictionary geliefert. Dieses wird ausgelesen, dabei allerdings anstelle des Wertes ein String abgespeichert, der die Klasse des Parameters enthält. Dies findet seinen Grund darin, dass die Klasse des Wertes je nach Notification ganz unterschiedlich auffallen kann. Das entnehmen Sie bitte der Dokumentation. … - (void)applicationDidFinishLaunching:(NSNotification*)notif { NSWorkspace* workspace = [NSWorkspace sharedWorkspace]; NSNotificationCenter* center = [workspace notificationCenter]; SEL catcherMethod = @selector( catcherMethod: ); [center addObserver:self selector:catcherMethod name:nil object:nil]; } …
Hier wird die vorgezeigte Empfängermethode installiert. Der Unterschied zu Band I (S. 514 f.) besteht lediglich darin, dass das Workspace-Notification-Center verwendet wird. … - (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication*)sender {
584
Kapitel 7
Die Applikation und ihre Umgebung
NSWorkspace* workspace = [NSWorkspace sharedWorkspace]; NSNotificationCenter* center = [workspace notificationCenter]; [center removeObserver:self]; return NSTerminateNow; } - (id)init { self = [super init]; if( self ) { self.eventDescriptions = [NSArray array]; } return self; } @end
Im Interface Builder ziehen Sie bitte einen Array-Controller in das Hauptfenster und binden das Content Array an die Eigenschaft eventDescriptions des Applika tionsdelegates. Ziehen Sie ein Tableview mit zwei Spalten in das Anwendungsfenster und binden Sie dessen Spalten an den Array-Controller mit den Schlüsseln date und name. Der rechten Spalte (date) geben Sie bitte einen Date-Formatter. Ziehen Sie nunmehr einen Dictionary-Controller in das Hauptfenster und binden Sie dessen Content Dictionary an die Selection des Array-Controller, wobei Sie als Schlüssel parameters verwenden. Legen Sie ein weiteres Tableview mit zwei Spalten in das Anwendungsfenster und binden Sie die Spalten an den Dictionary-Controller, wobei Sie die Schlüssel key bzw. value verwenden. Speichern. (Wir haben noch das Ganze in ein Splitview gesetzt und mit den Auosizing-Eigenschaften herumgespielt. Wenn Sie möchten, …) Wir haben jetzt also einen Tableview, der die Notifications als Liste anzeigt und einen weiteren, der das userInfo-Dictionary mit seinen Schlüsseln und Werten darstellt. Nützlich – oder nuttig, wie der Holländer sagt. Wenn Sie das Programm starten, wechseln Sie einmal zu einer anderen Applikation. Sie sehen, dass für Deaktivierung bzw. Aktivierung von – auch fremden – Applikationen Notifications gesendet werden. Spannender vielleicht: Schließen Sie ein externes Laufwerk an oder doppelklicken Sie auch eine DMG-Datei. Sie werden eine Notification erhalten, dass ein neues Laufwerk gemountet wurde. Das kann etwa bei Disktools hilfreich sein, wenn Ihre Applikation darüber informiert sein soll, welche Laufwerke aktuell existieren.
585
SmartBooks
Objective-C und Cocoa – Band 2
Dateioperationen Im Workspace finden Sie Dateioperationen, die mit Pfaden arbeiten und die Datei als Ganzes begreifen. Dies entspricht also dem File-Manager. Allerdings besteht hier auch die Möglichkeit, Operationen im Hintergrund durchführen zu lassen. Erstellen Sie sich bitte ein neues Projekt ohne Unterstützung von Core Data und Dokumenten, und nennen Sie es FManager.
Asynchrone Methoden Mit 10.6 existiert die Möglichkeit, mit -duplicateURLs:completionHandler: (NSWorkspace) eine Datei in ihrem Verzeichnis zu kopieren, also ein Duplikat zu erstellen. Die neue Datei enthält dabei den Namen der alten Daten mit einem angehängten » copy«. Falls dieser schon belegt sein sollte, wird zusätzlich eine laufende Nummer angehängt. Die Methode läuft parallel, hält also den Programmfluss in der sendenden Methode nicht auf. Man kann daher einen Block anhängen, der mit Abschluss der Operation ausgeführt wird und der als Parameter ein Dictionary mit Quell- und Zielnamen enthält und einen Fehlerparameter. Genaue Ausführungen zu Blocks erhalten Sie in der Referenz. Wir wollen diese günstige Gelegenheit jedoch wahrnehmen, schon einmal eine kleine praktische Einführung zu geben: In dem Application-Delegate deklarieren Sie bitte eine Action: @interface FManagerAppDelegate : NSObject { NSWindow *window; } @property (assign) IBOutlet NSWindow *window; - (IBAction)duplicateAsync:(id)sender; @end
Diese implementieren wir dann: @implementation FinderAppDelegate @synthesize window; - (IBAction)duplicateAsync:(id)sender { int result; NSOpenPanel* sourcePanel = [NSOpenPanel openPanel];
586
Kapitel 7
Die Applikation und ihre Umgebung
[sourcePanel [sourcePanel [sourcePanel [sourcePanel [sourcePanel [sourcePanel
setCanChooseFiles:YES]; setCanChooseDirectories:NO]; setAllowsMultipleSelection:YES]; setTitle:@"Choose Source"]; setMessage:@"Source filter to copy."]; setPrompt:@"Duplicate"];
NSString* startPath = NSHomeDirectory(); result = [sourcePanel runModalForDirectory:startPath file:nil types:nil]; if( result != NSOKButton ) { return; } NSArray* sourceFiles = [NSArray arrayWithArray:[sourcePanel URLs]]; if( [sourceFiles count] == 0 ) { return; } …
Bis hierin wird lediglich ein Open-Dialog geöffnet und die Auswahl des Benutzers abgeholt. … void (^handler)( NSDictionary*, NSError* ); handler = ^(NSDictionary* mapping, NSError* error) { NSLog( @"operation finished with %@", mapping ); }; …
Dies ist der oben angesprochene Block: Die erste Anweisung legt eine Blockvariable an, also eine Variable, die sozusagen Code aufnehmen kann. Durch die hinteren, runden Klammern wird spezifiziert, dass der Code wie eine Funktion zwei Parameter erwartet. Beachten Sie auch die Syntax mit dem isolierten Zirkumflex (^), welches die Variable handler zur Aufnahme eines Blocks typisiert (Blockvariable). In der zweiten Anweisung wird dann dieser Variablen ausführbarer Code zugewiesen. Dabei werden die Parameter benannt. Die erste Zeile dieser zweiten Anwei-
587
SmartBooks
Objective-C und Cocoa – Band 2
sung enthält also den Funktionskopf, der Rest den Funktionskörper. Wenn Sie also wollen, haben wir hier so etwas wie eine namenlose Funktion in einer Methode.
GRUNDLAGEN Dieses Ziel hätte man sicherlich auch mit Delegating erreichen können. Das Besondere an dem Block ist, dass er alle Variablen in seiner Umgebung kennt (also etwa die NSArray-Instanz sourceFiles). Werden diese im Block benutzt, so wird ihr Wert gemerkt. Eine nachträgliche Änderung beeinflusst also nicht den Wert in dem Block. Man kann sich das so vorstellen, dass alle lokalen Variablen von -duplicate: an den Block übergeben werden. Eine ausführliche Darstellung von Blocks finden Sie in der Referenz. … NSWorkspace* workspace = [NSWorkspace sharedWorkspace]; [workspace duplicateURLs:sourceFiles completionHandler:handler]; NSLog( @"message sent" ); }
Als Nächstes holen wir uns den Workspace und führen die Duplizierung aus. Den Handler übergeben wir dabei. Da die Operation nebenher läuft, landen wir sofort wieder in unserer Methode -duplicate. Der Block wird erst ausgeführt, wenn die Operation beendet ist. Wechseln Sie in MainMenu.xib und ziehen Sie einen Button in das Programmfenster. Verbinden Sie diesen mit der Actionmethode. Starten Sie das Programm und suchen Sie eine oder mehrere größere Dateien. Mit Duplicate starten Sie dann bitte den Vorgang. Achten Sie im Log auf die Reihenfolge der Meldungen: >… message sent >… operation finished
In die gleiche Kerbe schlägt übrigens die Methode -recycleURLs:completionHandler:, die Dateien in den Papierkorb verschiebt. Dies ist auch ein Unterschied zum File-Manager, der den Papierkorb nicht explizit kennt.
Synchrone Methoden Klassisch, das heißt bereits vor Snow Leopard, existieren Methoden, die Dateioperationen synchron vornehmen. Sie kehren also erst zurück, wenn die Operation abgeschlossen ist. Zentral ist hierbei die Methode -performFileOperation:source: destination:files:tag:, welche als ersten Parameter einen Operator nimmt. 588
Kapitel 7
Die Applikation und ihre Umgebung
Application Es kann konfiguriert werden, wie Ihre Applikation im System erscheint. Dies betrifft
• • •
das Dock das Statusmenü das Aktivierungsverhalten
Erzeugen Sie sich ein neues Projekt ohne Unterstützung für Dokumente und Core Data und nennen Sie es DockClock. Es handelt sich um eine kleine Uhr, die im Dock läuft.
Dock Eine wichtige Verbindung zwischen Applikation und Nutzer wird vom System über das Dock hergestellt. Zum einen kann man hiermit die Applikation aktivieren, zum anderen bietet es ein Menü an. Umgekehrt können auch in begrenztem Umfang Ausgaben angezeigt werden. Wie Sie wissen, besitzt jede Applikation – fast jede, wie wir sehen werden – ein Bild im Dock, das Dock-Icon. Sie werden ferner von einigen Applikationen her wissen, dass diese ihr Icon verändern, sei es durch Anzeige einer Zahl in einem roten, gezackten Kreis (Badge), sei es durch ein lebendiges Icon. Verwaltet wird das Dock seit OS X 10.5 durch ein Dock-Tile (Dock-Kachel). Badge Icon
Dock-Tile
Das Erscheinungsbild der Applikation im Dock wird durch das Dock-Tile bestimmt.
Das Dock-Tile erhält man mit –dockTile (NSApplication) für das Programmicon bzw. –dockTile (NSWindow) für ein einzelnes Fenster. Dieses kann ja auch ins Dock verschoben werden, indem man auf den Minimize-Button (Minuszeichen in der Titelzeile des Fensters) klickt. 589
SmartBooks
Objective-C und Cocoa – Band 2
Dockbadge Eine vergleichsweise einfache Übung stellt es dar, das Badge einzublenden. Wir werden hier einfach den Text »24« ausgeben, der die 24-Stunden-Anzeige darstellen soll. Sie können freilich die Applikation erweitern und dort etwa den Wochentag ausgeben, da das Dockbadge auch Zeichen ausgeben darf. Allerdings ist der Platz beschränkt, so dass etwa nur 6 Ziffern ausgegeben werden. Wir müssen lediglich zum Programmstart das Badge setzen. Freilich lässt es sich jederzeit aktualisieren: - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { NSApplication* application = [NSApplication sharedApplication]; NSDockTile* dockTile = [application dockTile]; [dockTile setBadgeLabel:@"24"]; }
Sie sehen hier einfach, wie das Dock-Tile der Anwendung abgeholt und gesetzt wird. Man muss es eben nur kennen.
HILFE Sie können das Projekt in diesem Zustand als Projekt »DockClock 1« von der Webseite herunterladen.
Dockimage Es existiert die Möglichkeit, mit –setApplicationIconImage: (NSApplicaiton) das Bild der Applikation im Dock zu setzen. Da es vor Mac OS X 10.5 keine weitere API zur Veränderung des Dock-Tiles gab, bediente man sich dieser Methode, um dynamische Anzeigen zu ermöglichen. Man erzeugte einfach mit -lockFocus (NSImage) ein entsprechendes Image im Code und setzte es. Inzwischen ist diese Möglichkeit aber wohl überholt. Jedenfalls wird aus diesem historischen Grund das Image eben über die Application-Instanz gesetzt.
Dockview Die nunmehr existierende Möglichkeit besteht darin, einen eigenen View in das Dock zu schieben. Dieses kann sich dann normal zeichnen. (Es gibt ein paar Eigenheiten wie etwa diejenige, dass man das Fenster des Views nicht ermitteln kann.)
590
Kapitel 7
Die Applikation und ihre Umgebung
Dies wollen wir an einem Beispiel verdeutlichen. Erzeugen Sie sich zunächst in Xcode eine neue Klasse aus der Vorlage Objective-C class mit der Basisklasse NSView und nennen Sie diese ClockView. Diese füllen wir auf: @interface ClockView : NSView { NSDate* date; NSDictionary* attribs; } @property( copy ) NSDate* date; @property( copy ) NSDictionary* attribs; @end
Die erste Eigenschaft dient zur Speicherung der anzuzeigenden Uhrzeit, die zweite lediglich dazu, Texteigenschaften auf Vorrat zu erzeugen. Kommen wir zur Implementierung. Diese ist aus gestalterischen Gründen recht umfangreich. Sie können natürlich die Dinge einfacher designen und etwa einfach einen roten Kreis ausgeben. Nicht bemalte Teile werden automatisch durchsichtig. const CGFloat border = -12; const CGFloat radius = 6; const CGFloat thickness = 8; @implementation ClockView @synthesize date; @synthesize attribs; - (void)drawRect:(NSRect)dirtyRect { NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; [dateFormatter setDateStyle:NSDateFormatterNoStyle]; [dateFormatter setTimeStyle:NSDateFormatterMediumStyle]; NSString* text = [dateFormatter stringFromDate:self.date]; …
Hier wird einfach mithilfe eines Date-Formatters aus dem gespeicherten Datum ein Text erzeugt. … NSRect textRect = self.bounds; CGSize size = [text sizeWithAttributes:self.attribs];
591
SmartBooks
Objective-C und Cocoa – Band 2
textRect.origin.x = NSMidX( textRect ) - size.width / 2.0; textRect.origin.y = NSMidY( textRect ) - size.height / 2.0; textRect.size = size; NSRect borderRect = NSInsetRect( textRect, border, border ); …
Ein paar Berechnungen, um die Größe des Views am Text auszurichten. Bitte beachten Sie, dass sich die Größe der Ausgabe mit der Methode –sizeWithAttributes: (NSStringDrawing) ermitteln lässt, wenn auch mit dieser Kategorie die Ausgabe selbst erfolgt (kommt gleich). Außerdem sei hier angemerkt, dass die Größe der Schrift in der Initialisierung der Einfachheit halber gesetzt wird. Es wäre natürlich möglich, sie anzupassen. Aber es geht hier nicht um Textausgabe. … NSBezierPath* border; NSGradient* gradient = [[[NSGradient alloc] initWithStartingColor:[NSColor whiteColor] endingColor:[NSColor darkGrayColor]] autorelease]; border = [NSBezierPath bezierPathWithRoundedRect:borderRect xRadius:radius yRadius:radius]; borderRect = NSInsetRect( borderRect, thickness, thickness ); NSBezierPath* background = [NSBezierPath bezierPathWithRect:borderRect]; [border appendBezierPath:background]; [border setWindingRule:NSEvenOddWindingRule]; [gradient drawInBezierPath:border relativeCenterPosition:NSMakePoint( -0.6, 1.0 )]; …
Auch diesen Code sollten Sie zwischenzeitlich selbst verstehen. Nur ein kleiner Trick sei verraten: Um die Umrandung außen abzurunden und nach innen eckig zu gestalten, wird ein Bezierpfad in den anderen gesetzt, und durch das Festlegen der Winding-Rule wird dafür gesorgt, dass der innere Pfad den äußeren »ausstanzt«. Schauen Sie sich das vielleicht noch einmal in Kapitel 1 an. Der Gradient dient dazu, einen Glanzeffekt zu erzeugen. 592
Kapitel 7
Die Applikation und ihre Umgebung
… NSColor* color = [NSColor blackColor]; color = [color colorWithAlphaComponent:0.5]; [color setFill]; [background fill]; …
Hier wird dann das innere Rechteck gesondert mit einem leicht transparenten Schwarz ausgegeben. … [text drawWithRect:textRect options:NSStringDrawingUsesLineFragmentOrigin attributes:self.attribs]; }
Zu guter Letzt erfolgt die Ausgabe der Uhrzeit selbst. Da wir die Attribute lesen, müsssen diese in -initWithFrame: gesetzt werden: - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { NSFont* font = [NSFont userFixedPitchFontOfSize:18.0]; NSColor* color = [NSColor greenColor]; NSShadow* shadow = [[[NSShadow alloc] init] autorelease]; [shadow setShadowColor:color]; [shadow setShadowOffset:NSZeroSize]; [shadow setShadowBlurRadius:8.0]; color = [color blendedColorWithFraction:0.4 ofColor:[NSColor whiteColor]]; NSMutableParagraphStyle* paragraphStyle = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease]; [paragraphStyle setAlignment:NSCenterTextAlignment];
593
SmartBooks
Objective-C und Cocoa – Band 2
self.attribs = [NSDictionary dictionaryWithObjectsAndKeys: font, NSFontAttributeName, color, NSForegroundColorAttributeName, shadow, NSShadowAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil]; self.date = [NSDate date]; } return self; }
Auch hier geschieht im Prinzip nichts Ungewöhnliches. Beachten Sie bitte nur den Trick, dass wir einen Schatten verwenden und diese Farbe dann etwas aufgehellt als Textfarbe benutzen. Hierdurch entsteht der Eindruck, dass die Schrift eine Aura um sich wirft. Chic! Um den View zu sehen, muss er in das Dock-Tile gesetzt werden. Dies erledigen wir im Application-Delegate: #import "DockViewAppDelegate.h" #import "ClockView.h" … - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { NSApplication* application = [NSApplication sharedApplication]; NSDockTile* dockTile = [application dockTile]; [dockTile setBadgeLabel:@"24"]; ClockView* dockView = [[[ClockView alloc] init] autorelease]; [dockView setDate:[NSDate date]]; [dockTile setContentView:dockView]; [dockTile display]; }
Auch das ist recht einfach, wenn man es weiß. Ein allerdings wichtiger Hinweis: Die Aktualisierung des Dock-Views wird nicht mit -setNeedsDisplay: (NSView) erledigt, sondern mit -display (NSDockTile). Bedenken Sie, dass der View nicht mehr der Applikation, sondern dem Dock gehört! Wenn Sie das Programm starten, sollten Sie schon das Wunderwerk Schweizer Uhrentechnik bewundern können. Vielleicht stellen Sie das Dock auf automatisch 594
Kapitel 7
Die Applikation und ihre Umgebung
vergrößern (Systemeinstellungen | Dock | Vergrößerung), damit Sie die Uhr gut erkennen können. Beachten Sie bitte auch, dass die Uhr recht weit oben angeordet werden muss, damit sie einen Bezug zum Badge hat. Dieser wird immer gleich in der oberen rechten Ecke platziert.
HILFE Sie können das Projekt in diesem Zustand als Projekt »DockClock 2« von der Webseite herunterladen. Sie werden allerdings auch sehen, dass sich die Uhr nicht automatisch aktualisert. Erledigen wir das noch mit einem Timer, der im Kapitel über Nebenläufigkeit genauer erläutert wird. Dazu benötigen wir zunächst eine Eigenschaft im Header: @interface DockViewAppDelegate : NSObject { NSWindow *window; NSTimer* timer; } @property( retain ) NSTimer* timer; @property (assign) IBOutlet NSWindow *window; @end
Und in der Implementierung: @implementation DockViewAppDelegate @synthesize window; @synthesize timer; - (void)tick:(NSTimer*)timer { NSApplication* application = [NSApplication sharedApplication]; NSDockTile* dockTile = [application dockTile]; ClockView* clockView = (ClockView*)[dockTile contentView]; [clockView setDate:[NSDate date]]; [dockTile display]; } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { …
595
SmartBooks
Objective-C und Cocoa – Band 2
SEL tickSelector = @selector( tick: ); self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:tickSelector userInfo:nil repeats:YES]; NSRunLoop* mainLoop = [NSRunLoop mainRunLoop]; [mainLoop addTimer:self.timer forMode:NSRunLoopCommonModes]; }
Jetzt sollte die Uhr durchlaufen.
HILFE Sie können das Projekt in diesem Zustand als Projekt »DockClock 3« von der Webseite herunterladen.
Dockmenü Zuletzt wollen wir unserer Uhr noch ein Menü geben. Es gibt grundätzlich drei Wege, dies zu erledigen:
•
Sie erzeugen in MainMenu.xib ein Menü und verbinden den File’s Owner, der ja eine Instanz von NSApplication ist, mit dem Menü über das Outlet dock Menu. Nicht sehr schwierig und häufig ausreichend.
•
Ähnlich ist der Ansatz einen neuen Nib-File aus der Vorlage Empty Nib. Danach fahren Sie wie oben fort. Das machen wir gleich.
•
Sie können im Application-Delegate die Methode -applicationDockMenu: (NSApplication) implementieren, die das Menü liefert. Der Witz besteht hier darin, dass sich das Menü leicht zur Laufzeit anpassen lässt.
In allen Fällen wird das bereit gestellte Menü dem bestehenden (System-)Menü hinzugefügt. Wir probieren das aus: Da wir ein Menü haben, müssen wir natürlich eine Actionmethode anbieten. Diese bauen wir in dem Application-Delegate ein. Im Header: @interface DockViewAppDelegate : NSObject { … }
596
Kapitel 7
Die Applikation und ihre Umgebung
… - (void)toggleDisplayMode:(id)sender; @end
In der Implementierung bauen wir lediglich eine Ausgabe ein, um den Erfolg zu überprüfen: @synthesize timer; - (void)toggleDisplayMode:(id)sender { NSLog( @"click" ); }
Erstellen Sie nun in Xcode eine neue Nib-Datei mit File | New File … Im folgenden Dialog wählen Sie links User Interface und rechts Empty XIB. Nennen Sie nach einem Kilck auf Next die Datei DockMenu. Öffnen Sie den Nib und erstellen Sie dort ein Menü mit dem Eintrag 24-hour mode. Jetzt haben wir also die Actionmethode und das dazugehörige Menü. Es gilt, zwei Verbindungen zu erzeugen: Der Applikation müssen wir mitteilen, wo sie das Menü findet. Hierzu wählen Sie den File’s Owner aus und setzen im Identity-Pane dessen Klasse als NSApplication. Wir werden gleich das Laden des Nib-Files besprechen. Die Angabe des File’s-Owners ist so richtig, wie Sie gleich sehen werden. Jetzt können Sie jedenfalls das Outlet dockMenu auf das neue Menü setzen. Außerdem müssen wir die Action ansprechen. Wir haben hier wieder das Problem, dass sich der Adressat – das Application-Delegate – nicht im Nib befindet. Daher müssen wir über die Resonder-Chain gehen: Dazu wählen Sie im Hauptfenster DockMenu.xib den First Responder aus und geben ihm im Attributes-Pane des Inspectors eine neue Actionmethode toggleDisplayMode:. Verbinden Sie jetzt den Eintrag im Menü mit dieser Actionmethode von First Responder. Dennoch, wenn Sie jetzt das Programm starten, erscheint bei einem Rechtsklick auf das Dock-Icon nicht unser neuer Eintrag. Dies findet seine Ursache darin, dass der Nib ja erst geladen werden muss. Automatisch geschieht dies nur mit dem MainMenu.nib, genauer mit demjenigen Nib, der in der Info.plist der Applikation unter dem Schlüssel Main nib file base name genannt ist und den Sie im Info-Window des Targets einstellen können (Band I, S. 683). Dies können wir freilich nicht ändern, da dieser Nib ja weiterhin geladen werden muss.
597
SmartBooks
Objective-C und Cocoa – Band 2
Der Trick besteht darin, dass man die Applikation dazu überreden kann, einen zweiten Nib zu laden. Dazu doppelklicken Sie bitte in Xcode in der Gruppe Resources auf den Eintrag DockClock-Info.plist. Im sich öffnenden Editor fügen Sie einen neuen Schlüssel AppleDockMenu ein und geben ihm als Wert den Namen unserer Nib-Datei ohne Suffix DockMenu. Nach dem Start der Applikation wird automatisch hiernach gesucht und die angegebene Datei geladen. Jetzt sollte auch ein Rechtsklick auf das Dock-Image den neuen Menüeintrag zeigen und, bei einem Klick hierauf, die Actionmethode ausgeführt werden.
HILFE Sie können das Projekt in diesem Zustand als Projekt »DockClock 4« von der Webseite herunterladen.
Aktivierung Mit dem Dock in Zusammenhang steht die Aktivierung der Applikation: Sie erfolgt häufig durch einen Klick auf das Dock-Icon. Umgekehrt gibt eine Applikation über das Dock bekannt, dass sie aktiviert werden möchte (hüpfendes Icon). Damit ist auch gleich etwas Wichtiges gesagt: Es ist unüblich, dass sich eine Applikation selbst aktiviert. Sie deaktiviert sich auch nicht. Vielmehr wird sie auf eine Benutzeraktion hin benachrichtigt, dass sie aktiviert oder deaktiviert werden soll. Den aktuellen Status einer Applikation kann man mit -isActive abfragen. Legen Sie bitte ein neues Projekt mit dem Namen ActivateDeactivate an, wobei Sie den Support für Core Data erneut abschalten, jedoch die Dokumentenunterstützung einschalten.
Delegatemethoden Das Application-Delegate kann einige Methoden implementieren, die die Aktivierung behandeln. Dies sind -applicationWillBecomeActive:, -applicationDidBecomeActive: , -applicationWillResignActive: und -applicationDidResignActive: (jeweils NSApplicationDelegate). Wie Sie jedoch an dem fehlenden Should im Methodennamen erkennen können, kann damit weder die Aktivierung noch die Deaktivierung beeinflusst werden. Aber überlegen Sie selbst, was Sie von einer Anwendung halten würden, die sich nicht in den Hintergrund bzw. Vordergrund klicken ließe …
598
Kapitel 7
Die Applikation und ihre Umgebung
Allerdings ist es bei Applikationen, die auf Dokumenten basieren, möglich, das Verhalten bei der Aktivierung zu ändern. Standardmäßig führt dies ja dazu, dass automatisch ein neues Dokument erzeugt wird, wenn keines geöffnet war. Um dies zu unterbinden, kann die Delegatemethode –applicationShouldHandleReopen: hasVisibleWindows: (NSApplicationDelegate) implementiert werden und NO zurückliefern. Natürlich ist es an dieser Stelle ebenso möglich, vor dem return selbst Arbeiten zu erledigen, etwa ein Fenster einzublenden. Das probieren wir an einem kleinen Beispiel aus: Zunächst müssen Sie ein Delegate als Subklasse von der Vorlage Objective-C class | NSObject erzeugen und mit AppDelegate benennen. Als Erstes verhindern wir nur, dass automatisch ein neues Dokument erzeugt wird: @implementation AppDelegate - (BOOL)applicationShouldHandleReopen:(NSApplication*)application hasVisibleWindows:(BOOL)visibles { return NO; }
Öffnen Sie bitte MainMenu.xib und ziehen Sie ein NSObject in das Hauptfenster. Dessen Klasse setzen Sie im Identity-Pane des Inspectors auf AppDelegate. Vom File’s Owner ziehen Sie nun eine Verbindung zum AppDelegate und setzen das Outlet delegate. Starten Sie die Applikation. Schließen Sie das bei Programmstart automatisch erzeugte Fenster. Wechseln Sie zu einem anderen Programm und dann wieder zurück. Sie sehen, dass nicht automatisch wieder ein Dokument geöffnet wird. (Es reicht auch aus, nach geschlossenem Fenster die Applikation einfach über das Dock zu reaktivieren.) Als zweiten Schritt zeigen wir dann einfach ein anderes Fenster modal an. Da es hier nur um die Demonstration der Delegatemethode geht, machen wir es uns besonders einfach. Wir fügen zwei Actionmethoden für ein modales Fenster ein und erweitern die Delegatemethode entsprechend zur Anzeige des Sheets. Nebenbei sehen Sie dabei auch noch eine besonders einfache Art der Anzeige modaler Fenster:
599
SmartBooks
Objective-C und Cocoa – Band 2
- (IBAction)abortTemplateSheet:(id)sender { [[NSApplication sharedApplication] abortModal]; } - (IBAction)executeTemplateSheet:(id)sender { [[NSApplication sharedApplication] stopModal]; } - (BOOL)applicationShouldHandleReopen:(NSApplication*)application hasVisibleWindows:(BOOL)hasvisiblesWindows { if( hasvisiblesWindows ) { return NO; } NSApplication* app = [NSApplication sharedApplication]; NSInteger response = [app runModalForWindow:templateSheet]; [templateSheet close]; if( response == NSRunStoppedResponse ) { NSDocumentController* docController = [NSDocumentController sharedDocumentController]; [docController newDocument:app]; } return NO; }
Die Actionmethoden machen wir im Header bekannt: @interface AppDelegate : NSObject { IBOutlet NSWindow* templateSheet; } - (IBAction)abortTemplateSheet:(id)sender; - (IBAction)executeTemplateSheet:(id)sender; @end
Erstellen Sie in MainMenu.xib ein neues Fenster, in welches Sie bitte zwei Buttons Cancel bzw. Execute ziehen. Die beiden Buttons verbinden Sie mit den neuen Actionmethoden. Sorgen Sie bitte noch dafür, dass im Attributes-Pane die Option Visible at Launch und Release When Closed ausgeschaltet sind. Wir wollen das Fenster ja nur auf eigene Anforderung, jedoch mehrfach öffnen. 600
Kapitel 7
Die Applikation und ihre Umgebung
Ziehen Sie schließlich die Connection templateSheet zu dem neuen Fenster. Starten Sie das Programm erneut und reaktivieren Sie es. Jetzt erscheint das Fenster. Je nach Buttton wird ein Dokument geöffnet oder nicht. Sie können auch bei Programmstart das Öffnen eines leeren Dokumentes mit der Delegatemethode -applicationShouldOpenUntitledFile: (NSApplicationDelegate) verhindern.
HILFE Sie können das Projekt in diesem Zustand als Projekt »ActivateDeactivate 1« von der Webseite herunterladen.
Anforderung Wie bereits erwähnt, ist es eine Aufgabe des Systems, für die Aktivierung zu sorgen. Es ist aber möglich, dies zu veranlassen, und zwar unter Mithilfe des Benutzers und automatisch. Aktivierung und Deaktivierung aus dem Code Hierzu dienen die Methoden -activateIgnoringOtherApps: und -deactivate. Die erste Methode nimmt dabei als Parameter die Anforderung, ob die eigene Applikation auch dann aktiviert werden darf, wenn eine andere bereits aktiviert ist. Das wird man meist wollen. -deactivate deaktiviert übrigens die Applikation, ohne sie in den Hintergund zu schieben. Beide Methoen sind außerordentlich unhöflich und sollten nur ausnahmsweise benutzt werden, wenn klar ist, dass der Benutzer die Applikation im Vordergrund haben möchte. Einen Fall werden wir gleich besprechen. Benutzeraufforderung Daneben existiert die wesentlich höflichere Möglichkeit, den Benutzer darum zu bitten, die Applikation zu aktivieren. Dies ist das Hüpf-Icon. Implementieren wir als Beispiel eine außerordentlich eifersüchtige Applikation, die sich meldet, wenn sie deaktiviert wurde. Dazu lassen wir das Icon im Dock hüpfen, was als User-Attention-Request (Benutzeraufmerksamkeitsanforderung – existieren nicht schöne deutsche Wörter) bezeichnet wird. Jede dieser Aufforderungen, die ja gehäuft auftreten können, bekommt ein NSInteger als ID. Diese speichern wir für die spätere Verwendung ab, was dazu führt, dass wir eine entsprechende Eigenschaft erzeugen:
601
SmartBooks
Objective-C und Cocoa – Band 2
@interface AppDelegate : NSObject { IBOutlet NSWindow* templateSheet; NSInteger userRequestID; } @property NSInteger userRequestID;
In der Implementierung: @implementation AppDelegate @synthesize userRequestID;
Zäumen wir das Pferd zur besseren Verständlichkeit von hinten auf: Zunächst implementieren Sie am Ende des Applicationdelegates die Methode, die uns über den Verlust der Aktivierung unterrichtet: - (void)applicationDidResignActive:(NSNotification*)notification { [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector( iMissYou: ) userInfo:nil repeats:NO]; } @end
Timer werden erst im Kapitel zur Nebenläufigkeit besprochen. Es dürfte hier aber klar sein, dass wir die Methode -iMissYou: (selector) des Applicationdelegates (target) nach 5 Seunden (TimeInterval) einmalig (repeats) aufrufen. Also muss diese Methode -iMissYou: implementiert werden, was Sie bitte der Delegatemethode voransetzen: - (void)iMissYou:(NSTimer*)timer { NSApplication* app = [NSApplication sharedApplication]; [app requestUserAttention:NSInformationalRequest]; [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector( iReallyMissYou: ) userInfo:nil repeats:NO]; }
602
Kapitel 7
Die Applikation und ihre Umgebung
Hier kommen wir zum eigentlichen Thema. In der zweiten Zeile des Methodenkörpers wird ein Informational-Request (informative Anforderung) erzeugt. Dies bedeutet, dass das Applikationsicon für eine Sekunde im Dock hüpft und dann automatisch aufhört. Das Gehüpfe würde übrigens auch dann aufhören, wenn wir innerhalb der Sekunde auf das Dock klicken. Das schafft allerdings der Benutzer zumeist nicht. Danach starten wir wieder den Timer und sagen ihm, dass in weiteren 5 Sekunden die Methode -iReallyMissYou: ausgeführt werden soll. Diese implementieren Sie bitte wiederum davor: - (void)iReallyMissYou:(NSTimer*)timer { NSApplication* app = [NSApplication sharedApplication]; self.userRequestID = [app requestUserAttention:NSCriticalRequest]; [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector( pft: ) userInfo:nil repeats:NO]; }
Es gibt hier zwei Unterschiede: Zum einen wird nunmehr ein Critical-Request (kritische Anforderung) erzeugt. Dies bedeutet, dass das nervige Gehüpfe erst aufhört, wenn entweder der Benutzer die Applikation aktiviert, er also der Aufforderung nachkommt, oder wenn wir vom Programm aus explizit den Request verwerfen. Da wir Letzteres gleich vornehmen, müssen wir uns zum anderen die ID des Requests merken.
AUFGEPASST Wenn in einer inaktiven Applikation eine modale Sitzung begonnen wird, erzeugt NSApplication automatisch eine kritische Anforderung. Zuletzt wird wiederum mit einer Verzögerung von 5 Sekunden die Methode -pft: ausgeführt, die Sie erneut davor implementieren: - (void)pft:(NSTimer*)timer { NSApplication* app = [NSApplication sharedApplication]; [app cancelUserAttentionRequest:self.userRequestID]; }
603
SmartBooks
Objective-C und Cocoa – Band 2
Da unser eifersüchtiger Partner nämlich nach 5 Sekunden kritischer Anforderung die Hoffnung aufgibt und sich jemand Neuen sucht, wird hier die kritische Anforderung wieder verworfen. Dies geschieht mit -cancelUserAttentionRequest:. Zu beachten ist dabei, dass die vorhin gemerkte ID hier verwendet wird. Wenn Sie das Programm starten und deaktivieren, sehen Sie, dass nach kurzer Zeit das Applicationicon kurz hüpft, nach weiterer Zeit dauerhaft hüpft, um dann schließlich sich wieder zu beruhigen.
GRUNDLAGEN Sie können in der kritischen Phase auch selbst das Gehüpfe beeenden, indem Sie die Applikation aktivieren. Wir berücksichtigen das hier nicht, weshalb der alte Timer noch läuft. Wie bereits erwähnt, widmen wir uns aber den Timern in einem gesonderten Kapitel.
HILFE Sie können das Projekt in diesem Zustand als Projekt »ActivateDeacitve 2« von der Webseite herunterladen.
Terminierung Schließlich gibt es noch den Fall, dass eine Applikation beendet wird. Für diesen Fall kann eine Applikation entscheiden, ob sie wirklich beendet werden möchte. Der Standardfall, nämlich dass noch offene Dokumente gesichert werden müssen, ist bereits implementiert. Aber es kann ja auch sein, dass Sie erst noch Daten auf einen Webserver schreiben wollen. Um einen entsprechenden Mechanismus zu implementieren, dient die Delegate methode -applicationShouldTerminate:. Sie kann dabei einen von drei Werten zurückgeben: NSTerminateNow führt dazu, dass die Applikation sich beendet. NSTerminateCancel lehnt die Terminierung ab. Schließlich kann mit NSTerminateLater bestimmt werden, dass die Applikation grundsätzlich zwar bereit ist, sich zu beenden, aber noch einen Moment benötigt. Im letzteren Falle muss irgendwann später -replyToApplicationShouldTerminate: ausgeführt werden, damit sich dann die Applikation beendet.
604
Kapitel 7
Die Applikation und ihre Umgebung
AUFGEPASST Wird der Rechner heruntergefahren, so versucht OS X, alle Applikationen zu terminieren. Sind diese nicht bereit, sich schnell zu beenden, wird der gesamte Vorgang des Herunterfahrens abgebrochen. Dies hat also auch Auswirkungen auf das gesamte System!
Statusmenübar Manche Applikationen möchten sich rechts in der Menüzeile einnisten. Es handelt sich dabei um die Status-Menu-Bar (Statusmenüzeile).
GRUNDLAGEN Dabei existieren zwei Typen von Status-Menüs: Wenn Sie bei gedrückter Befehlstaste [cmd] einen von Apple installierten Eintrag wie etwa die Zeit anklicken und die Maustaste halten, dann können Sie den Eintrag in der Menüzeile verschieben. Das sind Menü-Extras. Zum Vergleich können Sie sich etwa einmal den Tune•Instructor von Tibor Andre herunterladen. Auch seine Anwendung installiert sich im Statusmenü. Aber sein Eintrag lässt sich nicht verschieben. Das liegt daran, dass er die für normalsterbliche Programmierer ohne Apfeltattoo in der äußeren Großhirnrinde vorgesehenen offiziellen Status-Menüs verwendet. Es gibt nämlich keine dokumentierte Möglichkeit, Menü-Extras zu schreiben. Der Hintergrund ist, dass sich die Menü-Extras in einem eigenen Fenster des Systems befinden. Übrigens: Wenn Sie Donationware, also kostenlose Software, die sich über Spenden finanziert, verwenden, sollten Sie auch spenden. Auch Sie sind ja Entwickler … Eine solche Applikation will eigentlich zwei Dinge erzielen: Sie soll über die Statusmenübar schnell und einfach erreichbar sein und gleichzeitig in der Regel aus dem Dock verschwinden. Statusmenüs
Tune•Instructor-Statusmenu
Extramenüs
iChat-Extramenu
Die inof.plist enthält Informationen über die Erscheinung des Programmes.
Erzeugen Sie sich bitte eine neue Applikation mit dem Namen StatusMenu. Schalten Sie dabei Core-Data-Support und Dokumentenunterstützung aus. 605
SmartBooks
Objective-C und Cocoa – Band 2
Statusmenü Zunächst benötigen wir natürlich ein Menü, welches der Statusmenüzeile hinzugefügt werden soll. Dazu legen wir uns im Applicationdelegate ein Outlet an: @interface StatusMenuAppDelegate : NSObject { NSWindow *window; IBOutlet NSMenu* statusMenu; NSStatusItem* statusItem; } @property( retain ) NSStatusItem* statusItem;
Öffnen Sie MainMenu.xib. Ziehen Sie aus der Library ein Menü in das Hauptfenster. Den ersten Eintrag in dem Menü bezeichnen Sie mit Open Window und verbinden diesen mit der Actionmethode -makeKeyAndOrderFront: des Applikationsfensters im Hauptfenster MainMenu.xib. Den zweiten Eintrag bezeichnen Sie bitte mit Quit und verbinden ihn mit -terminate: der Applikation (nicht: Applikationsdelegate). Wählen Sie das Applikationsfenster aus und schalten Sie bitte im Attributes-Inspector die Option Visible At Launch aus, damit das Fenster erst auf unsere Anforderung geöffnet wird. Verbinden Sie nun das Outlet des Applicationdelegates mit dem neuen Menü. Damit das Menü erscheint, müssen wir es im Code in die Statusmenüzeile einbinden. Dies kann unmittelbar nach Programmstart in -applicationDidFinish Launching: des Applikationsdelegates erledigt werden: @implementation StatusMenuAppDelegate @synthesize statusItem; … - (void)applicationDidFinishLaunching:(NSNotification *)notif { NSStatusBar* statusBar = [NSStatusBar systemStatusBar]; NSStatusItem* item = [statusBar statusItemWithLength:NSVariableStatusItemLength]; [item setTitle:@"Status"]; [item setMenu:statusMenu]; self.statusItem = item; } …
606
Kapitel 7
Die Applikation und ihre Umgebung
Bitte beachten Sie hier zwei Eigenheiten: Zum einen wird das neue Item nicht mit +alloc/-init oder einen Convenience-Allocator erzeugt, sondern von der Statusbar. Außerdem beschreibt der Parameter nicht die Länge in Pixeln, sondern erhält eine Konstante, die eine variable Breite angibt. Alternativ werden wir hier gleich angeben, dass die Breite der Höhe entsprechen soll. (Es ist übrigens auch möglich, eine bestimmte Breite anzugeben. Jedoch ist dies nicht mehr so dokumentiert. Schließlich verwaltet das System die Einträge.) Außerdem ist es nicht Zufall, dass wir uns das Menü merken. Wir benötigen es nicht nur dazu, um es gleich wieder zu entfernen. Vielmehr verhält es sich zudem so, dass die Statusbar dem Menü kein retain schickt. (In unserem Falle würde allerdings das Menü durch den Nib-Loader gehalten.) Am Ende sollten wir das Statusmenü wieder entfernen: … - (void)applicationWillTerminate:(NSNotification *)notification { NSStatusBar* statusBar = [NSStatusBar systemStatusBar]; [statusBar removeStatusItem:self.statusItem]; } @end
Sie können bereits die Applikation starten und mittels Open Window ein Fenster öffnen. Auch lässt sich die Applikation mit Quit beenden. Beachten Sie aber, dass anders als beim Applikationsmenü der Eintrag sich nicht automatisch invertiert, wenn er angeklickt wird. Will man dies, so muss man das explizit mit -setHigh lightedMode: (NSStatusItem) angeben.
HILFE Sie können das Projekt in diesem Zustand als Projekt »StatusMenu 1« von der Webseite herunterladen. Es ist ebenfalls möglich, dem Statusmenü ein Bild oder ein View mitzugeben. Letzteres ist vor allem hilfreich, wenn man Stati anzeigen möchte. Erzeugen Sie sich bitte eine neue Klasse SemaphoreView aus der Vorlage Objective-C class | NSView. Dieser geben wir eine Eigenschaft, die animiert werden soll: @interface SemaphoreView : NSView { CGFloat offset;
607
SmartBooks
Objective-C und Cocoa – Band 2
} @property CGFloat offset; @end
Dazu die entsprechende Implementierung nebst Drawing: @implementation SemaphoreView @synthesize offset; - (void)drawRect:(NSRect)dirtyRect { [[NSColor greenColor] setFill]; NSRectFill( [self bounds] ); CGFloat x = self.offset; CGFloat alpha = fabs( (0.5 - x ) ) * 2.5 ; NSColor* darkColor = [NSColor colorWithCalibratedWhite:0.3 alpha:0.9]; NSColor* lightColor = [NSColor colorWithCalibratedWhite:0.3 alpha:alpha]; NSGradient* gradient = [[[NSGradient alloc] initWithColorsAndLocations:darkColor, 0.0, darkColor, x - 0.1, lightColor, x, darkColor, x + 0.1, darkColor, 1.0, nil ] autorelease]; [gradient drawInRect:[self bounds] angle:0.0]; }
Das ist graphisches Brimborium, wie Sie ja gleich sehen werden. Im Prinzip wird über ein grünes Rechteck ein Gradient gelegt, über den sich ein durchsichtiger Spalt bewegt. - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if( self ) { self.offset = 0.5; }
608
Kapitel 7
Die Applikation und ihre Umgebung
return self; } - (void) dealloc { self.offset = 0.0; [super dealloc]; }
Natürlich muss dieser neue View im Applikationsdelegate benutzt werden: @interface StatusMenuAppDelegate : NSObject { … CGFloat step; } … @property CGFloat step; @end
Die oben deklarierte Eigenschaft wird verwendet: #import "SemaphoreView.h" @implementation StatusMenuAppDelegate @synthesize step; … - (void)tick:(NSTimer*)timer { SemaphoreView* view=(SemaphoreView*)[[self statusItem] view]; CGFloat offset = view.offset + step; if( offset >= 0.9 ) { step = -step; offset = 0.85 + step; } if( offset … >… >… >… …
tick tack tick tack
Bedenken Sie aber bitte, dass -createThread: erst nach den kompletten Schleifendurchgängen in die Run-Loop zurückkehrt. Wir haben hier also echte Parallelität!
723
SmartBooks
Objective-C und Cocoa – Band 2
HILFE Sie können das Projekt in diesem Zustand als »Threads 1« von der Webseite herunterladen. Eine andere Methode, mit der Sie bequem Threads erzeugen können, ist -performSelectorInBackground:withObject: (NSObject). Das sähe dann so aus: SEL method = @selector( runInBackground: ); [self performSelectorInBackGround:method withObject:nil];
Manuelle Erzeugung Man kann freilich auch Instanzen von NSThread erstellen: Konzentrieren wir uns zunächst auf -createThread. Wir benutzen hier eine Klassenmethode, die einfach im System den Thread erzeugt und unsere Methode darin laufen lässt. Neben dieser sehr einfachen Art, eine Methode gleichzeitig laufen zu lassen, kann man auch explizit eine Instanz von NSThread erstellen. Der Vorteil dabei ist es, dass man diese entsprechend kontrollieren kann. Im Header fügen wir eine weitere Methode hinzu: @interface ThreadsAppDelegate : NSObject { NSThread* byTheWay; } @property( retain ) NSThread* byTheWay; - (IBAction)createThread:(id)sender; - (IBAction)cancelThread:(id)sender; @end
Speichern. Fügen Sie im Interface Builder einen weiteren Button hinzu, den Sie bitte mit Stopp beschriften. Ändern Sie -createThread: und fügen Sie eine neue Actionmethode hinzu. - (IBAction)createThread:(id)sender { SEL method = @selector( runInBackground: ); self.byTheWay = [[[NSThread alloc] initWithTarget:self selector:method object:nil] autorelease]; [self.byTheWay start]; }
724
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
- (IBAction)cancelThread:(id)sender { NSLog( @"cancel" ); [self.byTheWay cancel]; }
Natürlich müssen wir im -dealloc noch die Instanz freigeben: - (void) dealloc { self.byTheWay = nil; [super dealloc]; } @end
Sie können jetzt ebenso wie vorhin einen Thread starten.
Thread beenden Allerdings werden Sie enttäuscht feststellen, dass sich der Thread nicht mit einem Klick auf Stopp beenden lässt, obwohl wir ihm ja eine cancel-Nachricht schicken. Wenn Sie während des Laufes eines Threads erneut einen starten, so wird ebenfalls der alte nicht beendet. Immerhin gibt der Setter ja die alte Thread-Instanz frei, was jedoch den Thread nicht zu interessieren scheint. Die Gründe:
•
Die Thread-Instanz von Cocoa ist ebenso wie die Timer-Instanz nur ein Wrapper, der uns die Kommunikation mit dem Gegenstück im Betriebssystem erlaubt. Die Lebenszeit wird allerdings nicht geteilt. Daher muss ein Thread explizit beendet werden, die Löschung der Cocoa-Instanz reicht nicht aus.
•
Umgekehrt wird im System der Thread beendet, wenn die Methode beendet ist. Die Instanz ist weiterhin vorhanden.
•
Wenn wir wirklich einen Thread beenden wollen, müssen wir darum bitten, und er muss sich selbst beenden. Mit -cancel formulieren wir lediglich die Bitte.
Implementieren wir daher den Suizid des Threads auf Anforderung. Der Thread muss zwischendurch abfragen, ob eine Anforderung gekommen ist. Ist dies der Fall, so kann er sich beenden: - (void)runInBackground:(id)textField { NSAutoreleasePool* arp = [[NSAutoreleasePool alloc] init]; NSInteger index; for( index = 0; index < 10; index++ ) { if( [[NSThread currentThread] isCancelled] ) { NSLog( @"cancelling" );
725
SmartBooks
Objective-C und Cocoa – Band 2
break; } NSLog( @"tick" ); NSDate* awake = [NSDate dateWithTimeIntervalSinceNow:1.0]; [NSThread sleepUntilDate:awake]; } [arp release]; }
Um das Ganze auch beobachten zu können, lassen wir die Stoppmethode auf das Ende des Threads warten: - (IBAction)cancelThread:(id)sender { if( [self.byTheWay isExecuting] ) { NSLog( @"cancel" ); [self.byTheWay cancel]; while( ![self.byTheWay isFinished] ) { } NSLog( @"weg isser" ); } }
Wieso ist das so kompliziert? Zum einen kann der Thread Ressourcen belegt haben, die er wieder freigeben muss. Der einfachste Fall ist der Autorelease-Pool, den wir eingangs der Threadmethode erzeugt hatten und der wieder freigegeben werden muss. Ein weiteres Problem werden Sie ebenfalls nachvollziehen können, wenn Sie an die Eingangsproblematik denken, dass Ressourcen in Threads geschützt werden müssen: Wird ein Thread beendet, so muss er noch die Gelegenheit haben, alles wieder zu entsperren, was er vorher gesperrt hat.
GRUNDLAGEN Threads sollten grundsätzlich so konzipiert sein, dass sie nicht abgebrochen werden müssen. Meist liegt ein Design-Problem vor, wenn eine Beendigung eines Threads notwendig wird.
HILFE Sie können das Projekt in diesem Zustand als »Threads 2« von der Webseite herunterladen.
726
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
Threadfestigkeit von Cocoa Auch innerhalb eigener Threads wollen Sie freilich ständig Bestandteile von Cocoa benutzen. Dies ist nicht immer möglich. Wenn eine Technologie von Threads aus benutzt werden kann, bezeichnet man diese als »threadfest«. Und leider ist das so eine Sache …
Threads und AppKit Ein beliebter Fehler liegt darin, in einem Thread – genauer: in einem anderen Thread als dem Main-Thread – Veränderungen am User-Interface vorzunehmen. Dies erlaubt das AppKit nicht. Das Schlimme ist, dass so etwas ohne Weiteres gutgehen kann, bis man dann auf einmal zerschossene Darstellungen auf dem Bildschirm hat. Hieraus folgt, dass Zugriffe auf Elemente der Benutzerschnittstelle tabu sind. Offensichtlich liegt daher ein Verstoß vor, wenn etwa über ein Outlet ein Zugriff auf ein Textfeld erfolgt: - (void)threadMethode:(id)value { … // Nicht tun! [outletToATextField setStringValue:@"Text anzeigen"]; … }
Allerdings kann das auch verborgener liegen: Wenn wir Bindings einsetzen, dann wird eine Änderung am Model oder Controller ja bis zu Views weitergeleitet, die die geänderte Eigenschaft observieren. Dies bedeutet, dass das ahnungslose Ändern im Model zu einer Änderung der Benutzerschnittstelle führen kann. Um davon eine Vorstellung zu bekommen, erzeugen wir in unserem App-Delegate zwei Eigenschaften, threadsStarted und count: @interface ThreadsAppDelegate : NSObject { NSInteger threadsStarted; NSInteger count; NSThread* byTheWay; } @property NSInteger threadsStarted; @property NSInteger count; @property( retain ) NSThread* byTheWay;
727
SmartBooks
Objective-C und Cocoa – Band 2
An diese binden Sie die beiden Textfelder in MainMenu.xib, wobei das linke die Anzahl der gestarteten Threads zeigen soll. In der Implementierung synthetisieren wir die Accessoren dafür: @implementation ThreadsAppDelegate @synthesize byTheWay, threadsStarted, count;
Wir wollen jetzt die Eigenschaft threadsStarted in -createThread setzen, genauer: erhöhen: - (IBAction)createThread:(id)sender { // Mal eine andere Erzeugung: SEL method = @selector( runInBackground: ); [self performSelectorInBackground:method withObject:nil]; // Wert um eins erhoehen self.threadsStarted++; } … - (void) dealloc { self.threadsStarted = 0; self.count = 0; self.byTheWay = nil; [super dealloc]; } @end
AUFGEPASST Sie können auch die Eigenschaft byTheWay und die Actionmethode -cancel Thread: nebst Button wieder entfernen, weil wir das jetzt nicht mehr benötigen. Wir haben das im Samplecode auf der Webseite gemacht. Es stört aber auch nicht. Sie können bereits jetzt das Programm testen. Es sollte sich bei jedem Klick auf den Start-Button die entsprechende Anzeige erhöhen. Da diese Methode aber immer aus dem Main-Thread aufgerufen wird, müssen wir uns nicht um Probleme kümmern.
728
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
Ganz anders sieht das aus, wenn wir unsere Zählerei jetzt in den Thread verpacken. Wir wollen den Zählerstand im zweiten Label anzeigen. Da dessen Wert an die Eigenschaft des Controllers gebunden ist, würde eine Anweisung self.setCount = index;
Key-Value-Observing und endlich beim Label als View landen. Dürfen wir nicht! Der Trick besteht darin, nur das Setzen der Eigenschaft in den Main-Thread zu verlagern. Diese eine Methode wird also wieder im Main-Thread ausgeführt: - (void)setCountWithNumberObject:(NSNumber*)value { self.count = [value integerValue]; } - (void)runInBackground:(id)textField { NSAutoreleasePool* arp = [[NSAutoreleasePool alloc] init]; NSNumber* value; SEL update = @selector( setCountWithNumberObject: ); NSInteger index; for( index = 0; index < 10; index++ ) { value = [NSNumber numberWithInteger:index]; [self performSelectorOnMainThread:update withObject:value waitUntilDone:NO]; NSDate* awake = [NSDate dateWithTimeIntervalSinceNow:1.0]; [NSThread sleepUntilDate:awake]; } [arp release]; }
Wie Sie erkennen können, bedienen wir uns hierzu der Methode -performSelector OnMainThread:withObject: (NSObject). Da diese als Parameter ein Instanzobjekt erwartet, müssen wir unseren Integer entsprechend verpacken und im MainThread wieder auspacken. Üblicherweise hat man dieses Problem nicht, da ohnehin Objekte hin und her geschoben werden. Wir haben dieses Beispiel herausgesucht, um Ihnen gleich die Lösung zu demonstrieren. Wunderbar, jetzt fuhrwerkt der Thread nicht mehr am User-Interface herum.
HILFE Sie können das Projekt in diesem Zustand als »Threads 3« von der Webseite herunterladen.
729
SmartBooks
Objective-C und Cocoa – Band 2
Man kann auch auf die Abarbeitung der Methode im Main-Thread warten. Hier haben wir waitUntilDone auf NO gesetzt, damit das nicht erfolgt. Bauen Sie ein paar Logs ein: - (void)setCountWithNumberObject:(NSNumber*)value { NSLog( @"Update in Main-Thread" ); self.count = [value integerValue]; NSLog( @"beendet" ); } - (void)runInBackground:(id)textField { NSAutoreleasePool* arp = [[NSAutoreleasePool alloc] init]; NSNumber* value; SEL update = @selector( setCountWithNumberObject: ); NSInteger index; for( index = 0; index < 10; index++ ) { value = [NSNumber numberWithInteger:index]; NSLog( @"Verlasse Worker" ); [self performSelectorOnMainThread:update withObject:value waitUntilDone:NO]; NSLog( @"wieder im Worker" ); NSDate* awake = [NSDate dateWithTimeIntervalSinceNow:1.0]; [NSThread sleepUntilDate:awake]; } [arp release]; }
Sie können jetzt vermutlich im Log sehen, dass die beiden Threads ineinander verschränkt sind. Ein bisschen hängt das von Ihrem Computer und seiner Auslastung ab. Bei mir entsteht etwa Folgendes: >… >… >… >… …
Verlasse Worker wieder im Worker Update in Main-Thread beendet
Hier hat sich also das System dazu entschlossen, zunächst noch im Thread weiterzuarbeiten und erst später den Update im Main-Thread durchzuführen.
730
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
… >… >… >… >…
Verlasse Worker Update in Main-Thread wieder im Worker beendet
Hier erfolgt die Ausführung etwas verschränkt: Es wird mit dem Update im MainThread begonnen, dann aber im Worker-Thread fortgefahren und schließlich das Update im Main-Thread beendet. Sie sehen also, dass man sich da auf nichts verlassen kann – muss man in der Regel aber auch nicht. Ändern Sie jetzt einmal den Parameter waitUntilDone auf YES. Sie werden sehen, dass stets das Update im Hauptthread vollständig erfolgt und erst danach zurückgekehrt wird: >… >… >… >…
Worker Update in Main-Thread beendet wieder im Worker
Entfernen Sie bitte wieder die Logs und setzen Sie den Parameter waitUntilDone wieder auf NO. Es widerspricht ja dem Gedanken der Threads, wenn gegenseitig gewartet wird. Manchmal muss man das aber machen.
Threads und Core Data Auch Core Data ist nicht threadfest. Etwas anders ist die Lage hier aber: Sie können Core Data sperren, wenn Sie Änderungen an einem entsprechenden Model vornehmen wollen. Wie Sie Ressourcen im Einzelnen sperren können, lernen Sie ja noch. Hier nur das Problem an der Geschichte: Probleme bekommt der Kontext von Core Data. Dies gilt für jeden Zugriff auf das Model, da dieses wegen des automatischen Auslagerns und Nachladens zu einer Veränderung des Kontextes führen kann. Der Kontext war ja so etwas wie das Universum, in dem sich die geladenen Instanzen von NSManagedObject befinden. Hieraus resultiert, dass Sie bei jedem Zugriff auf Core Data den gesamten Kontext sperren müssen, und dementsprechend kein anderer Thread mehr darauf zugreifen kann. Mit Parallelität ist dann nicht mehr viel, wenn die Threads intensiv mit dem Model arbeiten. Es gibt Alternativen, die Sie daher bedenken sollten:
•
Sie geben der Thread-Instanz die Daten aufbereitet, etwa in einem Dictionary. Das Resultat wird, wie wir es gerade beim User-Interface gemacht haben, eben-
731
SmartBooks
Objective-C und Cocoa – Band 2
falls im Main-Thread zurückgeliefert, auch mit Dictionarys. Natürlich können Sie dabei wie oben auch Zwischenresultate liefern.
•
Sie transferieren die Daten mittels Distributed-Objects. Die funktionieren ja auch lokal.
Letztlich vereinfacht eine komplette Trennung des Datenmodells der Threads die Arbeit ungemein, auch wenn dies auf den ersten Blick umständlich erscheinen sollte.
Synchronisierungen und Locking Wenn Sie jedoch kein Core Data benutzen, so kann es durchaus sein, dass Sie aus unterschiedlichen Threads auf gemeinsame Ressourcen zugreifen wollen. So stellt etwa unsere Eigenschaft count eine Ressource dar. Starten Sie mal einen Thread, und während dieser läuft, starten Sie einen weiteren. Sie werden sehen, dass ziemliches Chaos auf dem Bildschirm entsteht. Überlegen wir uns, was geschieht: Jeder der beiden von Ihnen gestarteten Threads setzt diese Eigenschaft. Also erhält sie immer gerade den Wert, den zuletzt einer der beiden Threads gesetzt hat. Es ist eben wie mit unserer Textdatei: Die zwei kloppen sich. Man nennt dies eine »Race-Condition«. Wenn Sie übrigens einwenden wollen, dies sei ja kein Wunder, weil die zweite Methode (die Methode im zweiten Thread) nicht den zwischenzeitlichen Wert des ersten, bereits gestarteten Threads respektiert, so müssen wir Ihnen leider sagen, dass sich so einfach das Problem nicht reparieren lässt. Probieren wir es einfach einmal: - (void)runInBackground:(id)textField { NSAutoreleasePool* arp = [[NSAutoreleasePool alloc] init]; NSNumber* value; SEL update = @selector( setCountWithNumberObject: ); NSInteger index; NSInteger newCount = self.count; for( index = 0; index < 10; index++ ) { newCount++; value = [NSNumber numberWithInteger:newCount]; [self performSelectorOnMainThread:update withObject:value waitUntilDone:NO]; NSDate* awake = [NSDate dateWithTimeIntervalSinceNow:1.0]; [NSThread sleepUntilDate:awake];
732
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
} [arp release]; }
Wenn Sie jetzt den ersten Thread starten und bei 5 den zweiten Thread starten, ist das Endergebnis 15. Es müsste aber 20 lauten, da zwei Threads jeweils um 10 erhöht haben. Der Grund ist klar: Der zweite Thread holt den Wert bei 5 ab. Da er länger läuft, wird er den Endwert bestimmen. 5 plus 10 ergibt jedoch 15, so dass er diesen Wert als Letztes schreibt. Wir haben also die logische Eigenschaft count dreimal in unserem Programm: Einmal als wirkliche Eigenschaft und dann noch zweimal als lokale Variablen newCount. Es hilft übrigens nicht, auf diese zu verzichten und gleich die Eigenschaft mit self.count++
zu erhöhen. Mal abgesehen von dem (allerdings durch Verzicht lösbaren) Bindings problem bedeutet eine solche Erhöhung mindestens drei Anweisungen: Wert holen – Wert erhöhen – Wert schreiben. Und es kann, wenn es nur Satan will, passieren, dass gerade zwischen den Anweisungen der Thread unterbrochen und ein anderer gestartet werden. Und dann ist der geholte Wert schon wieder falsch.
GRUNDLAGEN Eine Operation, die sich wegen ihrer Subschritte teilen lässt, nennt man »nicht-atomar«. Umgekehrt wird eine Operation als »atomar« bezeichnet, wenn sie sich eben nicht unterbrechen lässt. Was wir hier machen, ist also letztlich, eine offensichtlich – weil aus mehreren Objective-C-Anweisungen bestehende – nicht-atomare Operation explizit pseudo-atomar zu machen. Die Alternative ist es zuweilen, spezielle atomare Funktionen zu verwenden. Das System bietet derlei Operationen wie OSAtomicIncrement32 an, welches etwa einen Wert ununterbrechbar erhöht. Sagen Sie nicht, dass der Fehler unwahrscheinlich sei: Es wird passieren. Und den Fehler dann zu finden, der nur so selten auftritt, ist wirklich eine Strafarbeit. Das ist also keine Lösung. Die Lösung liegt vielmehr darin, dass wir vor dem lesenden Zugriff sagen, dass wir jetzt an der Eigenschaft arbeiten wollen. Nachdem wir sie zurückgeschrieben haben. Wir sperren halt die Datei, die wir in einem TextEdit geöffnet haben.
733
SmartBooks
Objective-C und Cocoa – Band 2
Sie können sich das vorstellen wie in diesen doppeltürigen Sicherheitsschleusen: In den Zwischenraum gelangt man durch eine erste Tür und nur dann, wenn der Raum gerade leer ist. Dann schließt sich die Eingangstür. Keine zweite Person darf rein. Erst wenn die erste Person den Zwischenraum durch die zweite Tür wieder verlassen hat, darf der Nächste in den nun wieder leeren Raum. In diesem Sinnbild sind die Personen die Kontrollflüsse, in dem Raum befindet sich die Ressource, und die Anlage ist der Mutex.
Immer nur einer darf den Raum betreten, in dem sich das Gold in Form einer Ressource befindet.
734
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
Locking mit Objective-C Seit OS X 10.3 existiert die Möglichkeit, Sperren mit Sprachmitteln zu erzeugen. Es existiert das Schlüsselwort @synchronized, welches einen geschützten Block einleitet. Dieser Block muss dann die kritischen Anweisungen enthalten. Als Parameter erhält @synchronized einen Mutex, also eine Variable, die vereinfacht gesagt die doppeltürige Sicherheitsschleuse bezeichnet. Der Trick hierbei ist, dass jedes x-beliebige Objekt dafür herhalten kann. Dieses Objekt ist nur ein Name für den Raum. Bauen wir unseren Code entsprechend um: - (void)setCountWithNumberObject:(NSNumber*)value { self.count = [value integerValue]; } - (void)runInBackground:(id)textField { NSAutoreleasePool* arp = [[NSAutoreleasePool alloc] init]; NSNumber* value; SEL update = @selector( setCountWithNumberObject: ); NSInteger index; NSThread* thread = [NSThread currentThread]; // Beginn des kritischen Codes NSLog( @"Thread %@ will rein", thread ); @synchronized( self ) { NSLog( @"Thread %@ ist drin.", thread ); NSInteger newCount = self.count; for( index = 0; index < 10; index++ ) { … } NSLog( @"Thread %@ gibt auf.", thread ); } // synchronized NSLog( @"Thread %@ ist draußen.", thread ); [arp release]; }
Sie sehen also, dass vom Lesen bis zum letzten Zurückschreiben der Code von uns geschützt ist. Wenn Sie jetzt das Programm starten, werden Sie zwei Dinge bemerken: Zum einen können Sie ruhig einen zweiten Thread nach 5 Sekunden starten. Das Ergebnis wird beim erwarteten 20 landen.
735
SmartBooks
Objective-C und Cocoa – Band 2
HILFE Sie können das Projekt in diesem Zustand als »Threads 4« von der Webseite herunterladen. Zeitliche Granulierung Aber es gibt auch Bedauerliches zu berichten. Schauen Sie sich den Log an: >… >… >… >… >… >… >… >…
Thread Thread Thread Thread Thread Thread Thread Thread
{name
= = = = = = = =
(null), (null), (null), (null), (null), (null), (null), (null),
num num num num num num num num
= = = = = = = =
2} 2} 3} 2} 2} 3} 3} 3}
will rein ist drin. will rein gibt auf. ist draußen. ist drin. gibt auf. ist draußen.
Sie können erkennen, dass der zweite Thread erst dann die Freigabe erhält, wenn der erste komplett beendet ist. Dies ist ja auch klar, da wir erst am Ende des ersten Threads wieder den Mutex freigeben. Damit laufen aber beide Threads nacheinander und benötigen insgesamt 20 Sekunden. Das ist nicht das, was wir erreichen wollten! Der Trick liegt darin, die Bereiche des Lockings zu verkleinern. In unserer Methode verhält es sich so, dass die Erhöhung der Eigenschaft nur einen kleinen Teil darstellt. Die meiste Zeit wird im »virtuellen« Schlaf verbraten, der als Platzhalter für anderweitige Arbeit steht. Dieser Bereich muss aber nicht geschützt sein, da er nicht auf die gemeinsame Ressource zugreift. Bauen wir das entsprechend um: - (void)runInBackground:(id)textField { … NSThread* thread = [NSThread currentThread]; for( index = 0; index < 10; index++ ) { @synchronized( self ) { // Beginn des kritischen Codes NSLog( @"Thread %@ will rein", thread ); NSLog( @"Thread %@ ist drin.", thread ); self.count++; value = [NSNumber numberWithInteger:count]; [self performSelectorOnMainThread:update
736
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
withObject:value waitUntilDone:YES]; NSLog( @"Thread %@ gibt auf.", thread ); } // synchronized NSLog( @"Thread %@ ist draußen.", thread ); // Andere Arbeiten NSDate* awake = [NSDate dateWithTimeIntervalSinceNow:1.0]; [NSThread sleepUntilDate:awake]; } [arp release]; }
Wenn Sie jetzt mehrere Threads starten, werden Sie bemerken, dass die Zählung wirklich schneller läuft. Auch im Log sehen Sie, dass die einzelnen Threads ineinander verschränkt sind.
HILFE Sie können das Projekt in diesem Zustand »Threads 5« von der Webseite herunterladen. Sachliche Granulierung Wie bereits gesagt, bekommt der @synchronized ein Objekt mitgeliefert, welches eine Art Bezeichner, ein Name für den Mutex, ist. Wir haben hier self verwendet. Dies ist eine reine Vereinbarung: Jeder, der auf die Eigenschaft count dieser Instanz zugreifen will, muss den entsprechenden Mutex verwenden. Das müssen Sie als Programmierer selbst nachhalten. Hätten wir zwei Instanzen von AppDelegate, so hätten wir zwei verschiedene selfZeiger. Die Konsequenz ist, dass Threads wirklich parallel auf die Eigenschaft count dieser verschiedenen Eigenschaften zugreifen könnten. Wir hätten ja zwei verschiedene Mutexe, so als ob wir zwei Sicherheitsschleusen hätten. Das ist vorteilhaft, denn in der Tat stört es uns nicht, wenn irgendwo die gleiche Eigenschaft einer anderen Instanz verändert würde. Hätten wir mehrere Eigenschaften, so hätte unser Mutex aber den Nachteil, dass bei Zugriff auf eine Eigenschaft, nehmen wir unser count, auch der Zugriff auf alle anderen Eigenschaften unterbunden wäre. In diesen Fällen ist es denkbar, mehrere Mutexe anzubieten. Da diese pro Instanz bezogen sein müssen, lassen sie sich nicht mehr trivial erzeugen. Eine Klasse kann dafür eine Methode anbieten, wobei die ID der Instanz mit dem Namen der Eigenschaft kombiniert wird. Sie können eine 737
SmartBooks
Objective-C und Cocoa – Band 2
solche Instanz abweichend von den Speicherverwaltungsregeln auch lazy erzeugen. Beispiel: @interface someClass : NSObject { id countMutex; } @property( readonly ) id countMutex; … - (id)countMutex { if( countMutex == nil ) { countMutex = [NSString stringWithFormat:@"Mutex count for %p", self]; countMutex = [countMutex copy]; } return countMutex } - (void)dealloc { [countMutex release]; … [super dealloc]; }
Natürlich hindert Sie auch nichts daran, gleich im -init ganz normal den Mutex zu erzeugen und Setter zu verwenden. Wie dem auch sei, es darf nicht übersehen werden, dass die Gleichung »Eine Eigenschaft = Ein Mutex« falsch ist: Haben wir etwa eine abhängige Eigenschaft countText, die den aktuellen Wert als String enthält, so ist natürlich auch diese Eigenschaft zu sperren, wenn nur count verändert wird. Dies in zwei Zügen zu machen, also erst count sperren und ändern, dann countText sperren und ändern, kann dazu führen, dass unsere Daten in sich inkonsistent werden. Wir müssen hier also einen Mutex für beide Eigenschaften verwenden.
Locking mit Cocoa Neben der Möglichkeit des Lockings mittels @synchronize existieren auch noch in Cocoa die Klassen NSLock, NSRecursiveLock und NSConditionLock, die Locking ermöglichen.
738
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
GRUNDLAGEN Zuweilen findet man Hinweise darauf, dass Locking mit NSLock et al. günstiger sei als Locking mit @synchronized. Diese Vergleiche kranken aber daran, dass @synchronized nicht nur den Lock aufbaut, sondern diesen auch als rekursiven Lock (reentranter Code) erstellt und außerdem noch einen Exceptionhandler anlegt (exceptionfester Code, siehe hierzu auch die Referenz). Einen wirklichen Vergleich findet man dann aber dazu nicht. Der Vorteil von NSLock et al. scheint uns eher in dem sogleich erwähnten -tryLock zu liegen.
•
NSLock ist eher nachteilig, da es nicht rekursiv funktioniert. Wenn wir also etwa in unserer Methode count schon einem Lock unterziehen und dann eine Methode aufrufen, die das ebenfalls macht – sie weiß ja nicht, ob bereits ein Lock existiert –, dann bekommt sie dieses Lock nicht: Es befindet sich ja schon jemand in der Sicherheitsschleuse. Die Methode wartet also darauf, dass das Lock freigegeben wird , und der Aufrufer wartet drauf, dass die Methode zurückkehrt, bevor er freigibt. Man nennt diesen Zustand einen »Dead-Lock«.
•
NSRecursiveLock löst dieses Problem, indem es einen neuen Lock innerhalb desselben Threads erlaubt. Auch dann, wenn dieser Thread bereits gelockt hat.
•
Mit NSConditionLock kann die Erlaubnis zum Betreten an eine bestimmte Bedingung geknüpft werden. So kann etwa gewartet werden, bis im Lock ein bestimmter Wert erreicht wurde. Setzt den ein anderer Thread, so wird im ersten Thread der Lock erteilt und er wird fortgeführt.
Ein Vorteil dieser Klassen ist zudem, dass man anfragen kann, ob ein Lock erteilt werden wird: -tryLock (NSLock pp.). Kann eine Methode mehrere verschiedene Aufgaben gleichzeitig erledigen, so kann sie je nach Lage der Locks entscheiden, welche Aufgabe gerade bearbeitet werden kann. Außerdem kann man durch -lockBeforeDate: eine maximale Wartezeit bestimmen. Hier muss die Vereinbarung der Threads darauf lauten, dass alle denselben Lock verwenden. Also bietet sich hier ebenfalls eine entsprechende Instanzmethode an. Natürlich kann die Lockinstanz auch als Mutex in @synchronized verwendet werden. - (NSLock*)countLock if( countLock == nil ) { countLock = [[NSLock alloc] init]; }
739
SmartBooks
Objective-C und Cocoa – Band 2
return countLock } … // Statt mit @synchronized( countMutex ): [[self countLock] lock]; // kritischer Code [[self countLock] unlock];
Es sei aber eben darauf hingewiesen, dass im obigen Code keine Exception auftreten darf. In diesem Falle würde nämlich der Code den Lock nicht mehr freigeben und daher beim nächsten Aufruf blockieren.
Operation-Queues Mit OS X 10.5 wurden zwei neue Klassen, NSOperation und NSOperationQueue, eingeführt.
TIPP Mike Ash (von Rogue Amoeba) beschreibt auf seiner Webseite einen Fehler in der Klasse NSOperationQueue, der Abstürze verursachen kann. Der Fehler tritt allerdings sehr selten und nur bei Operationen, die eine sehr lange Zeit laufen, auf. Auf seiner Webseite bietet Mike Ash eine ähnlich funktionierende Alternative zu NSOperationQueue an. Apple hat mit OS X 10.5.7 den Fehler in NSOperationQueue beseitigt. Die Klasse NSOperation ist abstrakt. Dies bedeutet, dass eine Instanz von NSOperation zu fast nichts nützlich ist. Sie leiten in der Regel Klassen von NSOperation ab. Es ist üblich, für jede Klasse von Aufgaben eine Subklasse von NSOperation zu erstellen. Entwickeln Sie zum Beispiel ein Programm, welches große Matrizen berechnen kann, so könnten Sie ein NSOperation-Subclass für reelle und ein NSOperation-Subclass für komplexe Matrizen erzeugen. Für jeden Typ von Aufgaben sollten Sie folglich eine NSOperation-Subclass haben. Die Klasse NSOperationQueue arbeitet eng mit Instanzen von NSOperation zusammen. Das Wort »Queue« bedeutet hier Warteschlange. Eine Instanz von NSOperationQueue verwaltet eine Reihe von Instanzen von NSOperation. Mit -addOperation: können einer Operation-Queue neue Operation-Objekte hinzugefügt werden.
740
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
Operation A
Abgeschlossen
Operation B
Operation C Laufend Operation D
Operation E Wartend Operation F Operation-Queue
Eine Operation-Queue verfügt über wartende und ausführende Operationen. Bereits abgeschlossene Operationen werden automatisch aus der Operation-Queue entfernt.
Wird einer Operation-Queue mit -addOperation: eine neue Operation hinzugefügt, so wird die Operation unter Umständen nicht umgehend gestartet. In einer Operation-Queue gibt es nämlich wartende und laufende Operationen. Die Operation-Queue entscheidet selbstständig, wann welche Operation gestartet wird. Eine Operation-Queue kann natürlich mehr als zwei Operationen parallel ausführen. Vielleicht fragen Sie sich jetzt, wieso es in einer Operation-Queue überhaupt Operationen gibt, die warten müssen. Können nicht einfach alle Operationen einer Operation-Queue parallel laufen? Wieso wird die Anzahl parallel laufender Operationen beschränkt? Ganz einfach: Stellen Sie sich eine Operation-Queue vor, die standardmäßig jede zu ihr hinzugefügte Operation umgehend startet. Würden nun sehr viele Operationen zu dieser Operation-Queue hinzugefügt werden, so wäre das System fast alleine damit beschäftigt, die verfügbare Rechenzeit unter den vielen, vielen Operationen aufzuteilen. Die Operationen selbst kämen kaum noch dazu, ihre eigentliche Arbeit zu verrichten. Die optimale Anzahl der Operationen, die parallel ablaufen können, ist natürlich abhängig von der Hardware. Ganz grob kann man sagen, dass ein Computer, dessen CPU über acht Kerne verfügt, mehr Operationen parallel ausführen kann als ein Computer, dessen CPU über einen Kern verfügt. Eine Operation-Queue ermittelt selbstständig diese optimale Anzahl. Eine weitere Eigenschaft von Operation-Queues ist, dass zwei ihrer Eigenschaf741
SmartBooks
Objective-C und Cocoa – Band 2
ten, nämlich operations und maxConcurrentOperationCount, KVC und KVOcompliant sind. Die Eigenschaft operations liefert ein Array, welches alle laufenden und wartenden Operationen der Operation-Queue enthält. Die Eigenschaft maxConcurrentOperationCount gibt an, wie viele Operationen maximal parallel laufen dürfen, und hat standardmäßig den Wert NSOperationQueueDefaultMaxConcurrentOperationCount, was bedeutet, dass die Operation-Queue selbstständig feststellt, welches die maximale Anzahl parallel laufender Operationen ist. Dies ist schon deshalb vorteilhaft, weil die Systemlast zur Laufzeit dem Programmierer ja unbekannt ist.
Operation-Queues und eigene Operation-Subklassen Dieses Beispiel ist etwas komplexer, wovon Sie sich aber nicht abschrecken lassen sollten. Das Beispiel soll Ihnen zeigen, wie Sie
• • • •
eine NSOperationQueue erzeugen, eine Subklasse von NSOperation erstellen, neue Operationen einer Operation-Queue hinzufügen und die Operationen einer Operation-Queue und deren Eigenschaften observieren.
Im Rahmen dieses Beispiels werden Sie eine Anwendung entwickeln, die in der Lage ist, die Anzahl von Dateien in einem bestimmten Verzeichnis, deren Dateiname mit ».m« endet, zu ermitteln. Dabei können mehrere Verzeichnisse parallel analysiert werden. Jede Analyse eines Verzeichnisses wird in einem Tableview angezeigt. Damit Sie eine genauere Vorstellung davon bekommen, wie die Anwendung am Ende aussehen soll, betrachten Sie bitte die folgende Abbildung, die die Benutzerschnittstelle der Anwendung zeigt.
Eine dreispaltige Tabelle bildet die Benutzerschnittstelle der Anwendung.
742
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
Sie sehen links ein Tableview mit drei Spalten. Das Tableview soll die Operationen anzeigen, die sich aktuell in der Operation-Queue befinden. Jede Operation in der Operation-Queue verfügt über die Eigenschaft directoryPath, die den Pfad des Verzeichnisses, welches analysiert werden soll, enthält. Diese Eigenschaft wird in der Tableview in der ersten Spalte angezeigt. Die Eigenschaften isCancelled, isConcurrent, isExecuting, isFinished, isReady, dependencies und queuePriority der Klasse NSOperation sind allesamt KVC und KVO-compliant. Alle Operationen, die sich in der Operation-Queue befinden und aktuell ausgeführt werden, sollen durch einen Haken in der zweiten Spalte des TableViews entsprechend gekennzeichnet werden. Die Eigenschaft isExecuting einer jeden Instanz von NSOperation gibt an, ob die Operation gerade läuft oder nicht. Anhand dieser Eigenschaft können Sie der TableView sagen, in welcher Zeile sie in der zweiten Spalte einen Haken zu setzen hat. Die dritte Spalte soll fortwährend die Anzahl der bereits gefundenen Dateien mit der Endung .m anzeigen. Dazu wird in der Subklasse von NSOperation die Eigenschaft countOfImplementationFiles angelegt. Wie die Subklasse im Detail aussieht, sehen Sie gleich. Die Buttons auf der rechten Seite dienen zur Erzeugung neuer Instanzen der Subklasse von NSOperation. Ein Klick auf den Button Desktop startet die Suche nach Dateien mit der Endung .m, die sich im Desktop-Verzeichnis befinden. Entsprechend bewirkt ein Klick auf den Button Downloads das Starten der Suche nach Dateien mit der Endung .m, die sich im Download-Verzeichnis befinden. Analog gilt dies auch für die anderen Buttons. Subklasse von NSOperation Erzeugen Sie bitte ein neues Projekt vom Typ Cocoa Application, welches keine Unterstützung für Dokumente und Core Data enthält mit dem Namen Operation Queue. Legen Sie eine neue Klasse an, die Sie TraverseDirectoryOperation nennen, wobei Sie Objective-C class als Vorlage wählen. Öffnen Sie die Datei TraverseDirectoryOperation.h. Jede Instanz von TraverseDirectoryOperation soll über zwei Eigenschaften verfügen: Die Eigenschaft namens directoryPath legt den Pfad zu einem Verzeichnis fest, dessen Dateien, die mit .m enden, gezählt werden sollen. Die Eigenschaft countOfImplementationFiles enthält die aktuelle Anzahl der Dateien, die mit .m enden. Damit ergibt sich das folgende Interface der Klasse TraverseDirectoryOperation: @interface TraverseDirectoryOperation : NSOperation { NSString *directoryPath; NSNumber *countOfImplementationFiles; } #pragma mark Properties @property (copy) NSString *directoryPath;
743
SmartBooks
Objective-C und Cocoa – Band 2
@property (copy) NSNumber *countOfImplementationFiles; #pragma mark Creating Traverse Directory Operations - (id)initWithDirectoryPath:(NSString *)initDirectoryPath; @end
Findet eine Instanz von TraverseDirectoryOperation in ihrem zugewiesenen Verzeichnis eine Datei, die mit .m endet, so erhöht sie den Wert von countOfImplementationFiles um eins. Wechseln Sie nun zur Datei TraverseDirectoryOperation.m. Nun folgt die eigentliche Implementierung von TraverseDirectoryOperation. Es werden wie üblich die Eigenschaften synthetisiert und die Initialisierer implementiert. #import "TraverseDirectoryOperation.h" @implementation TraverseDirectoryOperation #pragma mark Properties @synthesize directoryPath, countOfImplementationFiles; #pragma mark Creating Traverse Directory Operations - (id)initWithDirectoryPath:(NSString *)initDirectoryPath { self = [super init]; if(self) { [self setDirectoryPath:initDirectoryPath]; NSNumber *zeroNumber = [NSNumber numberWithInt:0]; [self setCountOfImplementationFiles:zeroNumber]; } return self; } - (id)init { return [self initWithDirectoryPath:nil]; }
Die Klasse NSOperation definiert die Methode -main, welche automatisch von der Operation-Queue, zu der die Instanz von NSOperation gehört, aufgerufen wird, sobald die Operation ablaufen darf. Eine Subklasse von NSOperation sollte folglich ihre Arbeit, die sie zu verrichten hat, in der Methode -main implementieren. Genau das macht auch die Klasse TraverseDirectoryOperation. Eine Instanz von 744
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
TraverseDirectoryOperation untersucht in ihrer main-Methode die Dateiendung einer jeden Datei, die sich in ihrem Verzeichnis (directoryPath) befindet. Trifft sie dabei auf eine Datei mit der Endung .m, so erhöht die Instanz den Wert von count OfImplementationFiles um eins. #pragma mark NSOperation - (void)main { NSLog(@"start: %@", [self directoryPath]); NSFileManager *fileManager = [NSFileManager defaultManager]; NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtPath: [self directoryPath]]; NSString *file = nil; while (file = [enumerator nextObject]) { if(![[file pathExtension] isEqualToString:@"m"]) { continue; } NSUInteger currentNumber = [[self countOfImplementationFiles] unsignedIntegerValue]; NSNumber *newNumber = [NSNumber numberWithUnsignedInteger: currentNumber + 1]; [self setCountOfImplementationFiles:newNumber]; sleep(1); } NSLog(@"end: %@", [self directoryPath]); }
Die Aufrufe der Funktion NSLog am Anfang und Ende der Implementierung von -main sorgen nur dafür, dass Sie auch in der Konsole von Xcode das Geschehen nachvollziehen können. Die Inhalte des Verzeichnisses directoryPath werden durch die Klasse NSFileManager und die Methode -enumeratorAtPath: ermittelt. Ein Aufruf von -enumeratorAtPath: liefert einen Verzeichnisenumerator, mit Hilfe dessen Sie mit einer while-Schleife jeden Namen einer Datei, die sich in dem angegeben Verzeichnis befindet, als String erhalten. Wurde eine Datei mit der Endung .m ge745
SmartBooks
Objective-C und Cocoa – Band 2
funden, so wird der Wert von countOfImplementationFiles um Eins erhöht und sleep(1) aufgerufen. Der Aufruf von sleep(1) ist natürlich nicht notwendig. Er führt zu einer etwas langsameren Ausführung der Operation, so dass Sie genügend Zeit für Ihre Beobachtungen haben. Am Ende wird der Speicher wieder freigegeben. #pragma mark NSObject - (void)dealloc { [self setDirectoryPath:nil]; [self setCountOfImplementationFiles:nil]; [super dealloc]; } @end
Operation-Instanz herstellen Jetzt ist die Klasse TraverseDirectoryOperation fertig. Etwas komplizierter ist die Implementierung des Applikationsdelegates. Dieses soll dem Benutzer das Starten neuer Suchanfragen nach Dateien mit der Endung .m erlauben. Es führt die Suche mit Instanzen von TraverseDirectoryOperation durch, indem sie diese einer OperationQueue hinzufügt. Außerdem dient das App-Delegate als Data-Source des Tableviews, welches die Instanzen der Klasse TraverseDirectoryOperation, anzeigen soll, die sich in der Operation-Queue befinden. Damit ergibt sich folgendes Interface: #import @interface OperationQueueAppDelegate : NSObject { NSOperationQueue *operationQueue; NSTableView *operationsTableView; NSArray *oldOperations; } #pragma mark Properties @property (readonly, retain) NSOperationQueue *operationQueue; @property (retain) IBOutlet NSTableView *operationsTableView; #pragma mark Actions - (IBAction)traverseDesktop:(id)sender; - (IBAction)traverseDownloads:(id)sender; - (IBAction)traverseDocuments:(id)sender; - (IBAction)traverseDeveloper:(id)sender; - (IBAction)traverseLibrary:(id)sender; @end
746
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
Das Array oldOperations wird von dem App-Delegate lediglich intern benutzt. Dazu später mehr. Die Actionmethoden -traverse…: erzeugen entsprechende Instanzen von TraverseDirectoryOperation und fügen diese der Operation-Queue hinzu. Ein Aufruf von -traverseDesktop: würde beispielsweise eine Instanz von TraverseDirectoryOperation erzeugen. Speichern Sie die Datei OperationQueueAppDelegate.h. Erzeugen Sie ein dreispaltiges Tableview. Den Identifier der ersten Spalte setzen Sie auf directoryPath, den der zweiten Spalte auf isExecuting und den der dritten Spalte auf countOfImplementationFiles. Die Identifier werden Ihnen später beim Füllen der Tableview helfen. Ziehen Sie in die zweite Spalte eine Checkbox-Cell und in die dritte einen Number-Formatter. Erzeugen Sie fünf Buttons mit entsprechender Beschriftung und verbinden Sie die Buttons mit der entsprechenden Actionmethode des App-Delegates. Verbinden Sie das dataSource-Outlet des Tableviews mit der Instanz von AppController und das Outlet operationsTable View von AppController mit dem Tableview. Nun widmen wir uns der Implementierung von AppController. Öffnen Sie dazu die Datei AppController.m. Um immer über die Operationen in der Operation-Queue informiert zu sein, muss die Eigenschaft operations der Operation-Queue observiert werden. Zusätzlich interessiert sich der AppController für die Werte der Eigenschaften isExecuting und countOfImplementationFiles einer jeden Operation in der Operation-Queue. Es liegt also ein klassischer Fall der Observierung eines Arrays vor. Im ersten Band wurde bereits beschrieben, wie dies im Detail funktioniert. Wir werden daher in diesem Beispiel nur kurz auf die Technik der Observierung eines Arrays eingehen. #import "OperationQueueAppDelegate.h" #import "TraverseDirectoryOperation.h" static NSString *operationsContext = @"AppControllerOperationsContext"; static NSString *operationPropertiesContext = @"AppControllerOperationPropertiesContext";
Die beiden statischen Strings dienen als Kontext für Observierungen, die wir einrichten. Zum einen wird die Eigenschaft operations der Operation-Queue observiert und zum anderen gewisse Eigenschaften der Operationen. Der Kontext operationsContext wird bei der Observierung der Eigenschaft operations angegeben und der Kontext operationPropertiesContext bei den zu observierenden Eigenschaften. In einer Kategorie von AppController werden wieder einige Eigenschaften und Hilfsmethoden definiert.
747
SmartBooks
Objective-C und Cocoa – Band 2
@interface OperationQueueAppDelegate() #pragma mark Properties @property (readwrite, retain) NSOperationQueue *operationQueue; @property (copy) NSArray *oldOperations; - (void)startObservingOperations:(NSArray *)someOperations; - (void)stopObservingOperations:(NSArray *)someOperations; - (void)addTraverseDirectoryOperation:(NSString *)path; @end @implentation OperationQueueAppDelegate
Die Methode -startObservingOperations: beginnt die Observierung von gewissen Eigenschaften der übergebenen Operationen. Die Methode -stopObservingOperations: beendet die mit -startObservingOperations: begonnenen Observierungen. In der eigentlichen Implementierung des App-Delegates werden wie gewohnt die Eigenschaften synthetisiert und anschließend die Methoden -startObservingOperations: und -stopObservingOperations: implementiert. @implementation OperationQueueAppDelegate #pragma mark Properties @synthesize operationQueue, operationsTableView, oldOperations; - (void)startObservingOperations:(NSArray *)someOperations { for(TraverseDirectoryOperation *operation in someOperations) { [operation addObserver:self forKeyPath:@"isExecuting" options:0 context:operationPropertiesContext]; [operation addObserver:self forKeyPath:@"countOfImplementationFiles" options:0 context:operationPropertiesContext]; } }
748
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
- (void)stopObservingOperations:(NSArray *)someOperations { for(TraverseDirectoryOperation *operation in someOperations) { [operation removeObserver:self forKeyPath:@"isExecuting"]; [operation removeObserver:self forKeyPath:@"countOfImplementationFiles"]; } }
Die private Methode -addTraverseDirectoryOperation: erzeugt eine neue Instanz von TraverseDirectoryOperation, konfiguriert sie entsprechend und fügt sie der Operation-Queue hinzu. - (void)addTraverseDirectoryOperation:(NSString *)path { TraverseDirectoryOperation *operation = [[[TraverseDirectoryOperation alloc] initWithDirectoryPath:path] autorelease]; [[self operationQueue] addOperation:operation]; }
In der Methode -awakeFromNib werden vier Dinge erledigt:
• • • •
Es wird eine Operation-Queue erzeugt. Zu Demonstrationszwecken wird mit -setMaxConcurrentOperationCount: die Anzahl der maximal gleichzeitig ablaufenden Operationen auf zwei beschränkt. Das oldOperations-Array wird mit einem leeren Array initialisiert. Die Eigenschaft operations der Operation-Queue wird observiert.
- (void)awakeFromNib { NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease]; [self setOperationQueue:queue]; [[self operationQueue] setMaxConcurrentOperationCount:2]; [self setOldOperations:[NSArray array]];
749
SmartBooks
Objective-C und Cocoa – Band 2
[[self operationQueue] addObserver:self forKeyPath:@"operations" options:0 context:operationsContext]; }
In der Methode -observeValueForKeyPath:ofObject:change:context: werden, falls der Kontext der operationsContext ist, die neuen Operationen observiert und die alten nicht mehr observiert. Ist der Kontext der operationPropertiesContext, so wird lediglich das Tableview neu geladen. … #pragma mark KVO - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if([(NSString *)context isEqualToString:operationsContext]) { NSArray *newOperations = [object valueForKeyPath: keyPath]; NSMutableArray *onlyNew = [newOperations mutableCopy]; [onlyNew removeObjectsInArray:oldOperations]; [self startObservingOperations:onlyNew]; NSMutableArray *removed = [oldOperations mutableCopy]; [removed removeObjectsInArray:newOperations]; [self stopObservingOperations:removed]; [self setOldOperations:newOperations]; [operationsTableView reloadData]; [onlyNew release]; [removed release]; return; } if([(NSString*)context isEqualToString: operationPropertiesContext]) { [operationsTableView reloadData]; return; }
750
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } …
Die Actionmethoden -traverseDesktop:, -traverseDownloads:, -traverseDo cu ments:, -traverseDeveloper: und -traverseLibrary: nutzen zum Erzeugen neuer Instanzen von TraverseDirectoryOperation die private Methode -addTraverseDirectoryOperation:. … #pragma mark Actions - (IBAction)traverseDesktop:(id)sender { NSArray *paths = NSSearchPathForDirectoriesInDomains( NSDesktopDirectory, NSUserDomainMask, YES); [self addTraverseDirectoryOperation:[paths lastObject]]; } - (IBAction)traverseDownloads:(id)sender { NSArray *paths = NSSearchPathForDirectoriesInDomains( NSDownloadsDirectory, NSUserDomainMask, YES); [self addTraverseDirectoryOperation:[paths lastObject]]; } - (IBAction)traverseDocuments:(id)sender { NSArray *paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES); [self addTraverseDirectoryOperation:[paths lastObject]]; }
751
SmartBooks
Objective-C und Cocoa – Band 2
- (IBAction)traverseDeveloper:(id)sender { NSArray *paths = NSSearchPathForDirectoriesInDomains( NSDeveloperApplicationDirectory, NSUserDomainMask, YES); [self addTraverseDirectoryOperation:[paths lastObject]]; } - (IBAction)traverseLibrary:(id)sender { NSArray *paths = NSSearchPathForDirectoriesInDomains( NSLibraryDirectory, NSUserDomainMask, YES); [self addTraverseDirectoryOperation:[paths lastObject]]; } …
Da das Applikationsdelegate als Datenquelle des Tableviews agiert, muss AppController mindestens die Methoden -numberOfRowsInTableView: und -tableView:objectValueForTableColumn:row: implementieren. … #pragma mark NSTableView DataSource - (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView { return [[[self operationQueue] operations] count]; } - (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)tableColum row:(NSInteger)rowIndex { NSArray *operations = [[self operationQueue] operations]; TraverseDirectoryOperation *operation = [operations objectAtIndex:rowIndex]; NSString *identifier = [tableColum identifier]; if([identifier isEqualToString:@"directoryPath"]) { return [operation directoryPath];
752
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
} if([identifier isEqualToString:@"isExecuting"]) { return [NSNumber numberWithBool:[operation isExecuting]]; } if([identifier isEqualToString: @"countOfImplementationFiles"]) { return [operation countOfImplementationFiles]; } return nil; } …
Ganz zum Schluss wird der Speicher wieder freigegeben. … #pragma mark NSObject - (void)dealloc { [self setOperationQueue:nil]; [self setOperationsTableView:nil]; [self setOldOperations:nil]; [super dealloc]; } @end
HILFE Sie können das Projekt in diesem Zustand als »OperationQueue« von der Webseite herunterladen. Kompilieren und starten Sie die Anwendung. Stellen Sie sicher, dass beispielsweise auf Ihrem Desktop ein Verzeichnis existiert, welches einige Dateien enthält, die auf ».m« enden. Klicken Sie anschließend auf beliebige Buttons und beobachten Sie den Inhalt des Tableviews. Die Operation-Queue kümmert sich um die Ausführung der Operationen und das App-Delegate um die Observierung der Operationen und um die Versorgung des Tableviews mit aktuellen Daten. Das Beispiel ist komplex. Es verlangt von Ihnen Wissen über KVC, KVO, Delegating, Operationen 753
SmartBooks
Objective-C und Cocoa – Band 2
und Operation-Queues. Aber dieses Beispiel verdeutlicht, wie diese unterschiedlichen Technologien eingesetzt werden können, um relativ schmerzlos mit Threads umgehen zu können.
Tasks Einen Task startet man nicht ohne Grund. In der Regel erledigen Tasks Aufgaben und liefern ein Ergebnis. Am Ergebnis sind Sie natürlich brennend interessiert. Bevor wir uns mit Beispielen befassen zunächst noch einige grundlegende Anmerkungen zu Tasks. Mit Mac OS X haben Sie ein Unix-Betriebssystem vor sich. Unter Unix verfügt jeder Task über drei sogenannte Streams (dt. Ströme). Es gibt den Standardeingabe-Stream, den Standardausgabe-Stream und den Standardfehlerausgabe-Stream. Lassen Sie sich im Terminal mit dem Befehl ls den Inhalt des aktuellen Verzeichnisses ausgeben, so startet das Terminal den Task ls, schnappt sich dessen Standardausgabe-Stream und gibt ihn aus. Dieser Umstand verdeutlicht vielleicht schon den Zweck der unterschiedlichen Streams. Möchte ein Task ein Ergebnis melden, dann schickt er es in der Regel an den Standardausgabe-Stream. Bemerkt ein Task einen Fehler, dann schickt er zum Beispiel eine Fehlermeldung an den Standardfehlerausgabe-Stream. Über den Standardeingabe-Stream empfängt ein Task Daten, die zum Beispiel Anweisungen sein können. Diese Streams können umgeleitet werden. So kann die Standardausgabe eines Tasks in eine Datei umgeleitet und die Standardfehlerausgabe an einen weiteren Task geleitet werden, der dann eine SMS verschickt. Das Konzept der Streams findet sich im API zur Erzeugung und Kommunikation mit Tasks wieder, welches wir jetzt unter die Lupe nehmen wollen.
Erzeugen und Ausführen von Tasks Mit Hilfe der Klasse NSTask können Sie Tasks erzeugen und ausführen. Eine Instanz von NSTask muss lediglich wissen, welches Programm sie ausführen soll. Dies wird der Instanz mittels der Methode -setLaunchPath: mitgeteilt. Sie müssen dieser Methode einfach nur einen String übergeben, der den absoluten Pfad des auszuführenden Programms beinhaltet. Anschließend schicken Sie an den Task die Nachricht -launch, um diesen auszuführen. Hört sich einfach an. Ist es auch!
Launchpfad, Arbeitsverzeichnis und Argumente Als neugieriger Leser haben Sie vielleicht schon einen Blick auf die Dokumentation der Klasse NSTask geworfen und festgestellt, dass die Klasse über Methoden verfügt, mit denen ein Task vor seiner Ausführung konfiguriert werden kann. Das Programm /bin/ls listet die Inhalte des aktuellen Verzeichnisses auf. Eine Quizfrage: Falls Sie einen Task erzeugen, den Launch-Pfad auf /bin/ls setzen und den Task dann ausführen, was ist dann das aktuelle Verzeichnis? Probieren wir es doch 754
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
einfach aus. Erstellen Sie ein neues Foundation Tool, welches Sie bitte Execution Environment nennen. Öffnen Sie anschließend die Datei ExecutionEnvironment.m. Erzeugen Sie in der main-Funktion dann einen NSTask, setzen Sie den Launch-Pfad auf /bin/ls und geben Sie dann mittels NSLog das aktuelle Verzeichnis des Tasks aus. #import int main (int argc, const char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; NSTask *task = [[[NSTask alloc] init] autorelease]; [task setLaunchPath:@"/bin/ls"]; [task launch]; NSLog(@"aktueller Path: %@", [task currentDirectoryPath]); [pool drain]; return 0; }
HILFE Sie können das Projekt in diesem Zustand als »ExecutionEnvironment 1« von der Webseite herunterladen. Anhand der Ausgabe in der Konsole können Sie feststellen, dass das aktuelle Verzeichnis standardmäßig vom Prozess geerbt wird, der den Task ausgeführt hat. Mittels -setCurrentDirectoryPath: können Sie das aktuelle Verzeichnis neu festlegen, um so beispielsweise /bin/ls in Ihrem Heimatverzeichnis auszuführen. Neben dem aktuellen Verzeichnis können Sie einem Task Argumente mitgeben, dem Programm /bin/ls beispielsweise die Option -l (kleines L). Dies führt dazu, dass /bin/ ls neben den Namen der im aktuellen Verzeichnis befindlichen Dateien zusätzliche Informationen ausgegeben werden. Die Argumente eines Tasks könnten mittels -setArguments: festgelegt werden. Der Methode muss ein Array übergeben werden, welches keines, ein oder mehrere Argumente enthält. Probieren wir das mal aus. Passen Sie den Quellcode aus dem vorherigen Beispiel wie folgt an. #import int main (int argc, const char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
755
SmartBooks
Objective-C und Cocoa – Band 2
NSTask *task = [[[NSTask alloc] init] autorelease]; [task setLaunchPath:@"/bin/ls"]; [task setArguments:[NSArray arrayWithObject:@"-l"]]; [task launch]; [pool drain]; return 0; }
HILFE Sie können das Projekt in diesem Zustand als »ExecutionEnvironment 2« von der Webseite herunterladen. Führen Sie das Programm aus und vergleichen Sie die Ausgabe in der Konsole mit der vorherigen Ausgabe. Sie werden sehen, dass nun zusätzliche Details ausgegeben werden. Übrigens können Sie einen Task alternativ mittels der Klassenmethode +launchedTaskWithLaunchPath:arguments: erzeugen, konfigurieren und auch gleich ausführen.
Ausgabe eines Tasks empfangen Nehmen wir an, Sie möchten die Ausgabe eines Programms nutzen, um diese in einem Textview anzuzeigen. Um die Ausgabe eines Tasks anzuzapfen, müssen wir dem Task sagen, dass er seine Ausgabe an uns und nicht an die Konsole schicken soll. Dies geschieht mittels der Methode -setStandardOutput:. Ein Task erbt, wie auch das aktuelle Verzeichnis, die Standardausgabe vom Prozess, der den Task erzeugt hat. Wir sprachen eingangs von der Möglichkeit, mit Tasks kommunizieren zu können. Tasks sind aber etwas eigen und wählerisch. Man kann mit ihnen nicht einfach auf Zuruf kommunizieren. Sie müssen zunächst eine Leitung (engl. Pipe) zum Task legen. Pipes werden mittels +pipe (NSPipe) erzeugt. Sie arbeiten sehr eng mit File-Handles zusammen, die Sie ja schon kennen. Wir zeigen Ihnen jetzt, wie Sie mittels einer Pipe und eines File-Handles die Standardausgabe eines Tasks anzapfen. Legen Sie hierzu ein neues Projekt vom Typ Foundation Tool an. Nennen Sie das Projekt GrabOutput. Öffnen Sie die Datei GrabOutput.m und passen Sie den Code wie folgt an: #import int main (int argc, const char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
756
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
NSTask *task = [[[NSTask alloc] init] autorelease]; [task setLaunchPath:@"/bin/ls"]; // Das aktuelle Verzeichnis soll das Heimatverzeichnis sein. [task setCurrentDirectoryPath:NSHomeDirectory()]; // Pipe erzeugen und als Standardausgabe setzen [task setStandardOutput:[NSPipe pipe]]; [task launch]; // Es soll gewartet werden, bis der Task fertig ist. [task waitUntilExit]; // Jetzt holen wir uns die Leitung zum Task. NSPipe *output = [task standardOutput]; NSFileHandle *fileHandle = [output fileHandleForReading]; NSData *data = [fileHandle readDataToEndOfFile]; NSStringEncoding enc = NSUTF8StringEncoding; NSString *string = [[[NSString alloc] initWithData:data encoding:enc] autorelease]; NSLog(@"output: %@", string); [pool drain]; return 0; }
HILFE Sie können das Projekt in diesem Zustand »GrabOutput« von der Webseite herunterladen. Die wichtigen Stellen sind im Quellcode fett gedruckt. Wir hangeln uns von der Pipe über das File-Handle hin zu den Daten der Ausgabe von /bin/ls, mit denen dann ein String erzeugt wird. Den String könnten Sie dann in einem Textview anzeigen. Jetzt wissen Sie, wie man an das Ergebnis eines Tasks kommt. Mit ein wenig Kreativität fallen Ihnen vielleicht viele nützliche Einsatzgebiete an. Es gibt beispielsweise das Programm »mimetex«, welches aus einem LaTeX-String, der eine mathematische Formel enthält, ein Bild macht, welches die Formel zeigt. Sie könn757
SmartBooks
Objective-C und Cocoa – Band 2
ten jetzt eine Cocoa-Anwendung schreiben, die aus mathematischen Formeln Bilder erzeugen kann.
Asynchrones empfangen Manchmal liefert ein Task ein großes Ergebnis zurück. Ein riesiges Bild zum Beispiel. Oder die Ausführung eines Tasks dauert recht lange, und Sie möchten Ihr Programm für diese Zeit nicht blockieren. Dann müssen Sie dem Task beziehungsweise dem File-Handle der Standardausgabe mitteilen, dass die Ausgabe in kleinen Teilen zu erfolgen hat. Wie Sie mit File-Handles große Daten verarbeiten, wissen Sie ja schon. Wir zeigen Ihnen, wie Sie File-Handles gemeinsam mit Tasks nutzen. Legen Sie hierzu ein neues Projekt vom Typen Cocoa Application ohne Dokumente und Core Data an. Nennen Sie dieses Projekt bitte Async. Passen Sie den Quellcode wie folgt an: #import @interface AsyncAppDelegate : NSObject { NSMutableData *data; } @property (retain) NSMutableData *data; @end
Die Instanzvariable data wollen wir jetzt mit Hilfe des Programms /usr/bin/curl füllen. Mit /usr/bin/curl können unter anderem Daten aus dem Internet geladen werden. Geben Sie zum Testen im Terminal den Befehl curl http://www.cocoading. de ein. Das Programm /usr/bin/curl gibt den Quellcode der Seite http://www.cocoading.de aus. Diese Ausgabe wollen wir nun abfangen, und das auch noch asynchron. Wechseln Sie zur Datei AppController.m und passen Sie den Quellcode wie folgt an: #import "AsyncAppDelegate.h" @implementation AsyncAppDelegate @synthesize data; - (void)awakeFromNib { NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(handleOutput:) name:NSFileHandleReadCompletionNotification object:nil];
758
Kapitel 8
Run-Loops, Tasks, Threads: nebenher erledigt
[self setData:[NSMutableData data]]; NSString *cocoading = @"http://www.cocoading.de"; NSArray *args = [NSArray arrayWithObject:cocoading]; NSPipe *pipe = [NSPipe pipe]; NSFileHandle *handle = [pipe fileHandleForReading]; [handle readInBackgroundAndNotify]; NSTask *task = [[[NSTask alloc] init] autorelease]; [task setLaunchPath:@"/usr/bin/curl"]; [task setArguments:args]; [task setStandardOutput:pipe]; [task launch]; } - (void)handleOutput:(NSNotification *)note { NSDictionary *userInfo = [note userInfo]; NSString *key = NSFileHandleNotificationDataItem; NSData *outputData = [userInfo valueForKey:key]; if([outputData length] == 0) { NSString *text = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]; NSLog(@"text: %@", text); return; } [data appendData:outputData]; [[note object] readInBackgroundAndNotify]; } @end
HILFE Sie können das Projekt in diesem Zustand als »Async« von der Webseite herunterladen. Wie gewohnt erzeugen und konfigurieren wir einen Task und observieren die Notification NSFileHandleReadCompletionNotification. Der gesamte Ablauf sollte Ihnen bekannt vorkommen. File-Handles wurden ja schon behandelt.
759
SmartBooks
Objective-C und Cocoa – Band 2
760
Sie kommen nun so langsam ans Ende des Buches. Sie wissen alles, von dem wir wollten, dass Sie es wissen. Man darf davon ausgehen, dass Sie gute Software für den Mac schreiben können. Spätestens bevor Sie diese ausliefern, sollten allerdings einige Dinge erledigt werden, insbesondere sollte die Software auf Herz und Nieren geprüft werden. Zwei wichtige Möglichkeiten hierzu – Unit-Tests und Instruments – werden wir kurz ansprechen, indem wir eine exemplarische Sitzung darstellen und damit einen Einstig bieten. Das Bessere ist aber der Feind des Guten. Und natürlich ist die nächste Version Ihrer Software besser als die vorangegangene. Sie wollen auch möglicherweise mehrere Konfigurationen Ihrer Software verkaufen. Oder Sie haben gar ganz unterschiedliche Produkte, die allerdings auf einem Produkt basieren. In all diesen Fällen tauchen einerseits Probleme auf. Die Herausforderung besteht im Wesentlichen darin, diejenigen Teile Ihres Codes, die mehrfach verwendet werden sollen, nur ein Mal zu halten. Würde nämlich gleicher Code mehrfach vorhanden sein, so müsste er mehrfach gewartet werden. Es ist jedoch schwierig, dies synchron zu halten. Andererseits gibt es Möglichkeiten von Xcode, Objective-C und Cocoa, die Ihnen die Arbeit erleichtern. Diese wollen wir hier sammeln.
Auslieferung, Versionierung, Konfiguration
9
SmartBooks
Objective-C und Cocoa – Band 2
Softwareversionen Häufig kommt es vor, dass man verschiedene Versionen eines Programms hat. So sind etwa je nach Bezahlung verschiedene Ausbaustufen vorhanden oder die Programme sollen auf verschiedenen Rechnern mit unterschiedlichen Architekturen laufen. In all diesen Fällen verhält es sich jedoch so, dass der Großteil des Sourcecodes für alle Versionen aller Architekturen gleich ist. Es sind nur kleine Ecken, in denen Anpassungen vorgenommen werden müssen. Stellen Sie sich vor, ein AppDelegate zu haben, welches jede Menge Funktionalität in Actionmethoden enthält, von denen drei aber nicht in der Freeware-Version zur Verfügung stehen sollen. Es ist jetzt außerordentlich unbequem, sozusagen verschiedene Sourcecode-Versionen vorzuhalten, also Dateien AppDelegate_Free.h, AppDelegate_Free.m einerseits und andererseits AppDelegate_Full.h, AppDelegate_Full.m vorzuhalten und jeweils vor der Kompilierung auszutauschen. Denn wenn wir das Programm weiterentwickeln, so soll ja ebenfalls ein Großteil der Entwicklung in beide Versionen fließen. Wir müssen also die Arbeit doppelt machen, wenn man das auch noch mit verschiedenen Zielsystemen (ppc, Intel) und Architekturen (32 Bit, 64 Bit) kombiniert, kommt eine beachtliche Anzahl an Modulvarianten dabei heraus. Und spätestens hier ist der Zustand erreicht, dass die Angelegenheit nicht nur unpraktisch, sondern sogar fehlerträchtig wird. Um dies in den Griff zu bekommen, existieren verschiedene Technologien, die zusammenwirken:
• •
In Xcode können wir die Auswahl der gewünschten Konfiguration treffen. Auf der Ebene des Präprozessors können wir dafür Sorge tragen, dass aufgrund verschiedener Einstellungen verschiedene Programmversionen erzeugt werden.
Präprozessor Sie haben im ersten Band schon recht früh die Hauptaufgabe des Präprozessors kennengelernt: Er importiert Dateien in andere Dateien. Darüber hinaus kann man mit ihm viel anderes Sinniges und Unsinniges machen. Insbesondere Defines und bedingte Kompilierung sind ein beliebtes Einsatzgebiet – mit außerordentlich unterschiedlichem Zweckgehalt. Der Anfang einer Präprozessoranweisung wird durch ein Doppelkreuz # gebildet. Üblicherweise setzt man alle Präprozessoranweisungen an den Anfang der Zeile, also unabhängig von der Einrückung in der Objective-C-Source in Spalte 0. 762
Kapitel 9
Auslieferung, Versionierung, Konfiguration
POWER Grundsätzlich sei schon hier erwähnt, dass der Präprozessor zeilenorientiert arbeitet. Eine Anweisung hat also eine Zeile. Sie wird daher nicht mit einem Semikolon abgeschlossen, sondern mit dem Zeilenende. In diesem Abschnitt ist es daher wichtig, dass Sie den Umbruch der Zeilen nicht verändern. Sie können allerdings auch Präprozessoranweisungen mit einem umgekehrten Schrägstrich \ am Ende der Zeile über mehrere Zeilen verteilen. Die Versionierung einer Software über den Präprozessor funktioniert im Prinzip über zwei Technologien:
• •
Mit Defines werden Konfigurationen festgelegt. Mit bedingter Kompilierung werden die Sourcen an diese Festlegungen angepasst.
Defines Der Präprozessor lässt es zu, dass ein mit der Präprozessoranweisung #define festgelegter Bezeichner allein im Wege des stumpfen Textersatzes bei der Übersetzung ausgetauscht wird. Dies kann man dazu verwenden, bestimmte Versionen festzulegen. Zuweilen versteckt man auch Code in solchen Textschnipseln, was allerdings Nachteile hat. Man spricht dann auch von »Präprozessormakros«. Funktionsweise Bei der Verwendung von Defines erfolgt keine Überprüfung des Types oder des Kontextes, in dem der Bezeichner auftaucht. Vielmehr verhält es sich so, dass wie in der Funktion Suchen & Ersetzen eines Textverarbeitungsprogramms bei der Übersetzung alle Stellen ersetzt werden. // Makro mit dem Wert "3+5" definieren. #define MyMacro 3+5 … NSLog( @"%d", MyMacro ); // Verwenden des Makros
Noch vor der Kompilierung ersetzt also der Präprozessor den Text. Es wird folgender Sourcecode erzeugt: NSLog( @"%d", 3+5 );
Es ist ebenfalls möglich, Defines zu parametrisieren (Makro) und Defines in Defines zu verwenden: #define AnotherMacro( param ) MyMacro + param
763
SmartBooks
Objective-C und Cocoa – Band 2
Gefahren Hier entstehen allerdings zwei Probleme: Zunächst ist darauf hinzuweisen, dass die Source nach dem Textersatz alles ist, was der Compiler sieht. Er weiß nichts mehr von dem Makro. Er weiß nichts von der Herkunft. Aus diesem Grunde verschwinden die Makros auch im Debugger. Das ist bei der Fehlersuche nicht gerade dienlich. Das zweite Problem liegt darin, dass wirklich „3+5“ eingesetzt wird, nicht etwa 8. Deshalb sind jedoch Makros als ein gefährliches Spiel erkannt worden, so dass man weitestgehend darauf verzichtet. Auf der Webseite zum Buch finden Sie einen kleinen Aufriss von mir zu der Gefährlichkeit. Aber mal ein kleines Beispiel: #define MyMacro 3+5 … NSLog( @"%d", MyMacro * 2 );
Vielleicht erwarten Sie, dass 16 auf der Konsole erscheint, da 3+5 sicher 8 ergibt und dies dann ja im NSLog() mit 2 multipliziert wird. Pustekuchen! Wie gesagt, der Präprozessor ist dumm. Er erzeugt folgenden Code: NSLog( @"%d", 3+5 * 2 ); // Das war nicht gemeint
Und da in C die Regeln der Strich-vor-Punkt-Rechnung gelten, wird nunmehr 3 + 5 × 2, also 13 errechnet. Sie sehen: Das ist ein gefährliches Dingens, weil’s alles über den Haufen wirft, was wir an Mathematik kennen und einfach Texte ersetzt, blind, dumm.
GRUNDLAGEN Es gilt eine Grundregel, dass Präprozessoranweisungen für die Präprozessorverarbeitung benutzt werden können, es aber fehlerträchtig wird, Präprozessorcode wie die Definition eines Defines unmittelbar mit ObjectiveC-Code wie die obige Rechnung zu vermischen. Denn Präprozessorcode kennt die Sprache nicht, sondern nur die »Sprache des Präprozessors«. Er arbeitet also, wie bereits oben dargestellt, blind. Verwenden Sie also – wie wir gleich – Defines zur Steuerung des Präprozessors selbst, nicht aber in Ihrem Programmablauf. Defines können auch wieder wegdefiniert werden: #undef MyMacro
In dem folgenden Sourcecode ist dann MyMacro nicht mehr definiert. 764
Kapitel 9
Auslieferung, Versionierung, Konfiguration
Allerdings eignen sich Define ganz gut dafür, dass man Konfigurationen angibt. Hierzu wird ein Define entweder »leer« definiert, was erlaubt ist: #define Freewareversion
oder aber mit einem Wert, der gerade keine Verwendung in unserem Programm findet: #define AppVersion 0
Man kann dann diese Defines wie Konstanten für den Präprozessor ansehen: Sie werden nur innerhalb weiterer Präprozessoranweisungen benutzt, also von unserer Objective-C-Source strikt getrennt gehalten. Dann stellt sich freilich die Frage, wie ich solche Define benutzen kann. Dazu …
Bedingte Kompilierung Eine mögliche Methode, verschiedene Versionen einer Applikation zu warten, ist die Anwendung der bedingten Kompilierung. Hierbei ersetzt der Präprozessor automatisch verschiedene Programmfragmente in Abhängigkeit von Makros. Wesensmerkmal dieses Verfahrens ist, dass aus ein und demselben Sourcecode eine neue Version des ausführbaren Programms erzeugt wird. Für verschiedene Versionen ist daher der Übersetzungsvorgang mehrfach mit den entsprechenden Konfigurationen zu erzeugen. Gemeinsamer Code Freeware-Code Fullware-Code Gemeinsamer Code #define Version Free
#define Version Full Präprozessor
Präprozessor
Gemeinsamer Code Freeware-Code Fullware-Code Gemeinsamer Code
Gemeinsamer Code Freeware-Code Fullware-Code Gemeinsamer Code
Compiler
Compiler
Freie Version
Vollversion
Durch Defines kann der zu kompilierende Sourcecode ausgewählt werden.
765
SmartBooks
Objective-C und Cocoa – Band 2
Stellen Sie sich vor, dass Sie von Ihrem Programm eine Vollversion und eine Freewareversion haben wollen. In der Freeversion soll eine bestimmte Funktionalität nicht vorhanden sein, sondern bei deren Aufruf lediglich eine Fehlermeldung auf den Schirm zaubern. Das Problem dabei ist, dass Sie jetzt nicht zwei Anwendungen parallel entwickeln können. Jede Änderung der Sourcen, die beide Versionen betrifft, müssten Sie dann ja zweimal vornehmen. Abgesehen davon, dass das doppelte Arbeit ist, ist es schwierig, das immer zu synchronisieren. Daher bietet es sich an, in nur einem Sourcetext beide Versionen zu »verweben« und bei der Kompilierung automatisch aufgrund einer Voreinstellung die gewünschte herstellen zu lassen. Um dies zu verdeutlichen, erzeugen Sie sich bitte ein neues Projekt aus der Vorlage Application | Cocoa Application ohne Unterstützung von Dokumenten und Core Data und benennen dies PreXcodeVersion. In der Implementierung von PreXcodeVersionDelegate fügen wir etwas Sourcecode hinzu: … #define AppVersion 0 @implementation PreXcodeVersionAppDelegate @synthesize window; - (IBAction)coolFeature:(id)sender { #if AppVersion == 0 NSAlert* alert = [[[NSAlert alloc] init] autorelease]; [alert addButtonWithTitle:@"Schade!"]; [alert setMessageText:@"Eingeschraenkte Freewareversion"]; [alert setInformativeText:@"Wenn Sie dieses Feature nutzen \ wollen, muessen Sie schon Kohle \ rueberzocken!"]; [alert setAlertStyle:NSInformationalAlertStyle]; [alert runModal]; #elif AppVersion == 1 NSSound* sound = [NSSound soundNamed:@"Hero"]; [sound play]; #else #error Unknown Version! #endif }
766
Kapitel 9
Auslieferung, Versionierung, Konfiguration
Schauen wir uns das im Einzelnen an: In der ersten Zeile definieren wir ein Makro AppVersion, welches den Wert (»Textersatz«) 0 enthält. Dies wird zunächst eingesetzt: - (IBAction)coolFeature:(id)sender { #if 0 == 0 NSAlert* alert = [[[NSAlert alloc] init] autorelease]; [alert addButtonWithTitle:@"Schade!"]; [alert setMessageText:@"Eingeschraenkte Freewareversion"]; [alert setInformativeText:@"Wenn Sie dieses Feature nutzen \ wollen, muessen Sie schon Kohle \ rueberzocken!"]; [alert setAlertStyle:NSInformationalAlertStyle]; [alert runModal]; #elif 0 == 1 NSSound* sound = [NSSound soundNamed:@"Hero"]; [sound play]; #else #error Unknown Version! #endif }
Der Präprozessor ersetzt sodann alle unzutreffenden Zweige – und natürlich seine Anweisungen. Hiernach sieht der Sourcecode also so aus, da nur der #if-Zweig zutrifft: - (IBAction)coolFeature:(id)sender { NSAlert* alert = [[[NSAlert alloc] init] autorelease]; [alert addButtonWithTitle:@"Schade!"]; [alert setMessageText:@"Eingeschraenkte Freewareversion"]; [alert setInformativeText:@"Wenn Sie dieses Feature nutzen \ wollen, muessen Sie schon Kohle \ rueberzocken!"]; [alert setAlertStyle:NSInformationalAlertStyle]; [alert runModal]; }
767
SmartBooks
Objective-C und Cocoa – Band 2
Für den Compiler haben wir also nur ein Programm, welches einen Alert auf den Bildschirm bringt. Probieren wir das aus: Zunächst machen Sie bitte die Action im Header noch bekannt. Ziehen Sie im Interface Builder einen Button in das Hauptfenster und verbinden Sie diesen mit der Action coolFeature:. Starten und testen! Bei einem Klick auf den Button sollte die entsprechende Alert erscheinen. Gut, das war jetzt die eine Programmversion. Ändern wir oben die Version in 1: #define AppVersion 1
Jetzt trifft der #elif-Block zu und der Compiler bekommt folgendes Programm zu sehen: - (IBAction)coolFeature:(id)sender { NSSound* sound = [NSSound soundNamed:@"Hero"]; [sound play]; }
hier wird also ein Sound abgespielt. Wieder übersetzen und ausprobieren: Klicken Sie jetzt auf den Button, so hören Sie einen Ton. (Das ist unser tolles Feature, welches wir uns bezahlen lassen wollten. Glücklicherweise ist das hier kein Marketingbuch.) Sollte keine der Bedingungen zutreffen, so wird der Code aus dem #else-Zweig verwendet. Hier haben wir einen weiteren Präprozessorbefehl eingebaut, das #error. Dieses erzeugt einfach die entsprechende Fehlermeldung und bricht den BuildProzess ab. Sie können ja mal 2 als Version einsetzen und eine Übersetzung versuchen. In Errors and Warnings erscheint error: #error Unknown Version
Neben der #if-Kaskade bestehend aus #if Bedingung1 Anweisungen, falls Bedingung1 zutrifft #elif Bedingung2 Anweisungen, wenn Bedingung1 nicht, aber Bedingung2 zutrifft #elif Bedingung3 …
768
Kapitel 9
Auslieferung, Versionierung, Konfiguration
#else Anweisungen, falls keine der Bedingungen zutrifft #endif
existiert noch eine #ifdef-Kaskade: #ifdef Makro // Anweisungen, wenn Makro definiert wurde #else // Anweisungen, wenn Makro nicht definiert wurde #endif
Hierbei wird nicht der Wert eines Makros abgefragt, sondern ob es vorher mit #define definiert wurde. Alternativ kann man auch eine Präprozessorversion-Funktion defined() benutzen, was Apple macht (weshalb wir das hier erwähnen): #if defined( Makro ) … #endif
Die üblichen Boolschen Operationen wie ! (logisches Nicht), && (logisches Und) und || (logisches Oder) dürfen verwendet werden, ebenso wie die Vergleichsoperatoren ==, !=, = und die mathematischen Grundoperationen. #if defined( AppVersion ) && (AppVersion == 0)
Xcode-Unterstützung Sie sehen also, dass die Konfiguration des Sourcecodes über Makros und bedingte Kompilierung läuft. Im Prinzip könnten wir uns jetzt eine Konfigurationsdatei bauen, in der Markos für verschiedene Ausbaustufen unserer Software, Zielsysteme usw. definiert sind. Diese importieren wir dann in jedes Modul, so dass sich dieses bei Bedarf danach richten kann. Es geht aber noch einfacher:
Defines aus dem Build-Prozess Löschen Sie zunächst einmal im App-Delegate die Zeile, in der das Define definiert wird. Wenn Sie jetzt übersetzen und testen, bemerken Sie, dass ein solch undefiniertes Makro im #if als 0 interpretiert wird. Wir erhalten also die Freewareversion unserer Anwendung.
769
SmartBooks
Objective-C und Cocoa – Band 2
Gut, wir wollen auf etwas anderes hinaus: Wie bereits angesprochen, können Makrodefinitionen auch aus Xcode »importiert« werden. Der Compiler besitzt dazu den Schalter -D, welcher im User-Interface von Xcode unterstützt wird. Klicken Sie auf den obersten Eintrag PreXcodeVersion in Groups & Files und öffnen Sie das Infofenster. Wechseln Sie auf den Tab Build und geben Sie im Suchfeld Macro ein. Sie erkennen dann in der Gruppe GCC 4.00 – Preprocessing zwei Compilerschalter, die mit Macros zusammenhängen:
Auch aus Xcode heraus lassen sich Makros definieren.
Beide Einstellungen haben eigentlich dieselbe Bedeutung: Es wird ein Makro definiert, als ob Sie ein #define in die Source gepackt hätten. Im ersten Falle gilt das für jede Kompilierung. Im zweiten Falle wird die Definition für jene Header nicht durchgeführt, die sich im Precompiled-Header befinden.
GRUNDLAGEN Precompiled-Header (PCH) sind Dateien, die von Xcode gemeinsam vorkompiliert werden. Dabei werden alle in diesem Header gefundenen Importe anderer Dateien aufgelöst. Man erhält also einen monolithischen Block an Headern anstelle vieler kleiner Importe. Dies beschleunigt die folgenden Kompilierungen ganz enorm. Allerdings sollten sich deshalb in dem PCH nur Dateien befinden, die sich selten ändern, also nicht Ihre ständig bearbeiteten Anwendungsklassen. Die von uns verwendeten Templates erzeugen standardmäßig einen Prefix-Header, der sich unter dem Namen Projektname_Prefix.pch in der Gruppe Other Sources befindet. Wenn Sie diesen öffnen, erkennen Sie, dass lediglich Cocoa.h einbezogen ist, den Sie ja in der Tat nicht ändern. Cocoa.h allerdings importiert wiederum drei Header: Foundation.h, AppKit.h und CoreData.h, AppKit.h enthält wiederum 180 (!) Importe … Sie sehen also, dass es sich lohnt, das einmal zu sammeln und wieder zu verwenden.
770
Kapitel 9
Auslieferung, Versionierung, Konfiguration
NS… NS…
NS… NS…
NS… NS…
AppKit
Foundation
CoreData
PCH
PCH
Cocoa.h
Klasse1.h
PCH
Klasse1.m
Precompiling
…
KlasseN.h
KlasseN.m
Compiling
Der PCH wird nur ein Mal erstellt und dann immer wieder verwendet.
Da wir das Makro lediglich für unsere Sourcen benötigen, können wir also in der Zeile Preprocessor Macros not used in Precompiled Headers unsere Definition eintragen. Einfach einen Doppelklick ausführen. Im sich öffnenden Sheet erzeugen Sie mit dem +-Button einen neuen Eintrag und tippen dort AppVersion=1 ein. Das entspricht also nach dem Gesagten #define AppVersion 1
im Sourcecode und sollte eine Vollversion unserer Anwendung, also mit Sound, erzeugen. Probieren Sie es!
AUFGEPASST Jede Änderung der Build-Settings führt dazu, dass die PCH neu kompiliert werden (müssen). Dies können Sie bei der Übersetzung in der Statusleiste unten im Projektfenster ersehen. Ebenso wie Sie auch im Sourceode »leere« Definitionen erzeugen können, funktioniert dies auch hier, wenn Sie einfach den Teil ab = weglassen. Ein AppVersion entspricht also einem #define AppVersion
Gut, wir haben es jetzt also schon geschafft, aus Xcode die Version unserer Anwendung zu setzen. 771
SmartBooks
Objective-C und Cocoa – Band 2
Build-Konfigurationen Aber richtig bequem ist das noch nicht. Wie wir bereits erwähnten, kann es ja einen ganzen Satz an Konfigurationen geben, etwa für die Architektur oder den Prozessor-Typ. Daher wäre es praktisch, wenn man das automatisch umschalten könnte, anstatt jedes Mal die Definitionen selbst einzugeben. Und genau das geht. Bereits im ersten Band hatte ich die beiden vorgefertigten Konfigurationen Release und Debug angesprochen. Sie können sich selbst Konfigurationen erstellen.
Xcode erlaubt es, eigene Konfigurationen zu erstellen.
Die aktuelle Konfiguration dient zum Debugging der Vollversion, da wir ja das Makro AppVersion auf 1 gesetzt hatten. Erzeugen wir uns also ein weiteres, welches zum Debuggen der Freeversion taugt. Dazu wählen Sie bitte wieder den obersten Eintrag PreXcodeVersion in Groups & Files an und klicken auf Info. Diesmal wählen Sie aber den Tab Configurations. Zunächst einmal wählen Sie den obersten Eintrag Debug aus und ändern den Namen anhand des Buttons Rename in Debug (Vollversion). Dann klicken Sie auf Duplicate, um sich davon eine Kopie zu erzeugen. Diese benennen Sie bitte mit Debug (Freeware). Wechseln Sie jetzt wieder auf den Tab Build und wählen Sie oben die neue Konfiguration Debug (Freeware) aus. Suchen Sie nach dem Makro und setzen Sie es diesmal auf 0. Schließen! Wir haben also jetzt zwei Debug-Konfigurationen erzeugt, die das Makro AppVersion auf 0 bzw. 1 gesetzt haben. (Wir müssten dasselbe noch einmal mit der Release-Version machen, wenn wir das Programm wirklich ausliefern wollten. Sie können das selbst als Übung machen.) In dem Projektfenster können wir entsprechend in der Werkzeugleiste unter Active Build Configuration die aktuelle Konfiguration auswählen. Wenn Sie dies aus der Werkzeugleiste entfernt haben, funktioniert es ebenfalls über Project | Set Active Build Configuration. Wählen Sie jeweils eine der beiden Konfigurationen aus und testen Sie das Ergebnis nach Übersetzung. Bevor Sie nach dem Umschalten übersetzen, müssen Sie aller772
Kapitel 9
Auslieferung, Versionierung, Konfiguration
dings ein Clean All Targets durchführen, damit die alte Version gelöscht wird. Sie sehen, dass jeweils eine andere Fassung erzeugt wird.
POWER Die im entsprechenden Build-Fenster einzutragenden Einstellungen können auch in einer xcconfig-Datei abgelegt werden. Diese werden auch von Xcode bei Copy & Paste verwendet. Allerdings verzichten Sie dann auf die Bequemlichkeiten, die das Fenster bietet, wie etwa eine Liste der zulässigen Einstellungen. Das Format von xcconfig-Dateien entnehmen Sie bitte der Dokumentation zu Xcode, falls Sie diese verwenden wollen.
Target-Konfiguration Eine weitere – und für unser Beispiel sogar passende – Möglichkeit ist es, sich mehrere Targets zu erzeugen. Zunächst einmal öffnen Sie bitte wieder den KonfigurationenDialog und löschen eine der beiden Debug-Konfigurationen. Die andere benennen Sie wiederum auf Debug ohne jeglichen Zusatz um. Wechseln Sie nun wieder zu den Build-Einstellungen und suchen Sie nach dem Makro. Wählen Sie die entsprechende Zeile an und klicken Sie dann unten links auf das Pop-Up-Menü. Dort wählen Sie den Eintrag Delete Definition at This Level. Jetzt sollte wieder alles so wie vorher sein. Wie ich bereits im ersten Band ausführte, können Sie Build-Einstellungen für das gesamte Projekt vornehmen, indem Sie in der Projektleiste den obersten Eintrag selektieren. Das hatten wir bisher gemacht, weshalb unser Makro für das gesamte Projekt galt. Das meint auch der Menüeintrag mit »at this level«: auf Projektebene. Es ist aber ebenfalls möglich, für einzelne Targets Einstellungen vorzunehmen. Und es ist möglich, eigene Targets zu erzeugen: Suchen Sie bitte in der Projektleiste nach dem Eintrag Target und darin nach unserem Target PreXcodeVersion. Mit einem [ctrl]-Klick können Sie es duplizieren (Duplicate im Pop-Up-Menü). Bitte nennen Sie jetzt die alte Version PreXcodeVersion (Vollversion), die untere ebenso, bloß wiederum mit Freeware.
Man kann auch Einstellungen für ein bestimmtes Target in allen Build-Configurations wählen.
773
SmartBooks
Objective-C und Cocoa – Band 2
Wählen Sie jetzt die Vollversion an und öffnen Sie wieder die Build-Einstellungen. Achten Sie darauf, dass im obersten Pop-Up-Menü Configurations der Eintrag All Configurations ausgewählt ist. Wir wollen ja jetzt eine Einstellung für dieses Target vornehmen, welche aber sowohl in der Debug- wie auch der Release-Konfiguration vorhanden ist. Wir unterscheiden also nunmehr nach Targets anstelle von BuildConfigurations, Geben Sie wie vorhin bei den projektbezogenen Einstellungen für die Vollversion AppVersion=1 an. Sie werden bemerken, dass der Eintrag fett wird. Damit will Xcode Sie darauf aufmerksam machen, dass diese Einstellung von den Projekteinstellungen abweicht. Schließen. Wählen Sie nun das Freeware-Target an und geben Sie – wenig überraschend – in den Build-Einstellungen an der entsprechenden Stelle AppVersion=0 für die eingeschränkte Version an. Sie können nun in der Projektleiste von Xcode mit dem Pop-Up-Menü Active Target die Version auswählen. Sollte sich das Menü nicht in Ihrer Werkzeugleiste befinden, so geht es ebenfalls über das Programmmenü Project | Set Active Target. Testen Sie beide Versionen aus! Ich will Sie aber auf noch etwas aufmerksam machen: Wenn Sie in der Projektleiste Groups & Files die Gruppe PreXcodeVersion | Products öffnen, sehen Sie, dass es zwei gleichnamige Produkte gibt. Unsere Targets heißen aber unterschiedlich! Das ist neu. Der Grund dafür ist, dass zwar standardmäßig der Name unseres Targets dem unseres Produktes entspricht, aber eben nur standardmäßig. Sie können das überprüfen, indem Sie noch einmal das Infofenster für ein Target öffnen. In dem Tab General sehen Sie den Target Name, der lediglich ein Name für die Seitenleiste ist. Wenn Sie auf den Tab Properties klicken, sehen Sie, dass die Variablen EXECUTABLE_NAME und PRODUCT_NAME verwendet werden. In dem Build-Tab können Sie nach Product Name suchen und erkennen dort den alten Namen ohne jeglichen Zusatz. Das ist auch richtig: In aller Regel sollen die verschiedenen Versionen beim Kunden denselben Namen tragen.
AUFGEPASST Wie der EXECUTABLE_NAME im Einzelnen erzeugt wird, habe ich bereits in Band 1 im Kapitel über Xcode erläutert. Hier sei lediglich erwähnt, dass PRODUCT_NAME als Grundlage dient.
774
Kapitel 9
Auslieferung, Versionierung, Konfiguration
Versionierung von Models und Datenmigration mit Core Data Im ersten Band haben Sie Core Data kennengelernt. Vielleicht haben Sie zwischenzeitlich schon eine Anwendung mit Hilfe von Core Data entwickelt. Nehmen wir weiter an, dass Sie diese Anwendung weiterentwickeln. Bei der Weiterentwicklung werden Sie wohl auch Änderungen am Model Ihrer Anwendung vornehmen. Das Model einer Anwendung wird in der Regel aus zwei Gründen geändert. 1. Die Anwendung soll zusätzliche Informationen speichern, die bisher noch nicht berücksichtigt wurden. 2. Das Model der Anwendung soll beispielsweise durch eine bessere Normalisierung optimiert werden. Die iterative Entwicklung einer Anwendung bedingt die häufige Änderung des Models. Das Ändern eines Models ist also nichts Ungewöhnliches. Dennoch hat die Änderung des Models einer Anwendung weit reichende Konsequenzen. Ein Model beschreibt die Struktur von Daten. Wird diese Beschreibung nun geändert, können bestehende Daten nicht richtig interpretiert werden. Betrachten wir eine einfache Beispielanwendung, welche die Problematik verdeutlicht. Diese Anwendung verwaltet Personen. Das Model der Anwendung besteht folglich aus einer Entität namens »Person«. Diese Entität verfügt über ein Attribut namens »name«. In diesem Attribut wird der Name der jeweiligen Person gespeichert. Im Laufe der Zeit wächst der Bestand der verwalteten Personen. Nun soll die Anwendung Personen anhand des Vornamens oder des Nachnamens finden können. Grundlage für diese Funktionalität ist, dass Vorname und Nachname der Personen als eigenständige Attribute im Model vorhanden sind. Nichts leichter als das. Das Attribut »name« wird gelöscht und durch die Attribute »firstName« und »lastName« ersetzt. Aber was passiert jetzt mit dem alten Datenbestand? Dieser ist ganz anders strukturiert. Das neue Model ist zum alten Model inkompatibel. Eine mögliche Lösung wäre, die Namen bestehender Personen in Vorname und Nachname aufzuteilen. Aus der Person mit Namen »Christian Kienle« würde dann eine neue Person mit Vornamen »Christian« und Nachnamen »Kienle«. Seit OS X 10.5 ist Core Data in der Lage, Entwickler bei der Implementierung der notwendigen Datenmigration zu unterstützen. Sie werden jetzt lernen, wie Sie mit Hilfe von Core Data Datenmigrationen durchführen.
775
SmartBooks
Objective-C und Cocoa – Band 2
Versionierung von Models Bei dem Update einer Anwendung kommt es häufig vor, dass sich das Core DataModel ändert. Dann lässt sich standardmäßig nicht mehr die Datei in der alten Version des Models lesen. Dies ist ein Umstand, der Ihre Benutzer nicht zu Jubelschreien anregen wird. Die Lösung hierfür heißt Datenmigration mit Core Data.
Übersicht Um eine Datenmigration durchführen zu können, muss das alte sowie das neue, abgeänderte Model, bekannt sein. Seit OS X 10.5 unterstützt Core Data versionierte Models. Ein versioniertes Model ist einfach nur ein Ordner mit der Endung »xcdatamodeld«, der ein oder mehrere Models (Dateien mit der Endung »xcdatamodel«) enthält. Neben den Models enthält ein versioniertes Model noch eine Property-List, die die aktuelle Version festlegt.
POWER Die Property-List heißt ».xccurrentversion« und wird vom Finder standardmäßig nicht angezeigt. Sie müssen sich um den Inhalt dieser Datei in der Regel nicht kümmern. Das übernimmt Xcode für Sie. Sollten Sie sich aber fragen, wo Xcode die Information des aktuellen Models speichert, wissen Sie jetzt Bescheid.
Modelversionen erzeugen Ein versioniertes Model erzeugen Sie am besten mit Xcode. Markieren Sie in Xcode das Model, welches Sie versionieren möchten. Wählen Sie dann im Menü von Xcode den Menüpunkt Design | Data Model | Add Model Version aus. Xcode erzeugt dann ein versioniertes Model, verschiebt das ausgewählte Model in das neu erzeugte versionierte Model und erzeugt eine Kopie des ausgewählten Models. Außerdem wird das ursprünglich ausgewählte Model mit einem kleinen, grünen Haken gekennzeichnet. Dies bedeutet, dass dieses Model das aktuelle Model ist.
Ein versioniertes Model, welches zwei Versionen eines Models enthält. Das Model, welches mit dem grünen Haken markiert ist, wird als die aktuelle Version des versionierten Models angesehen.
776
Kapitel 9
Auslieferung, Versionierung, Konfiguration
Nun haben Sie also zwei Versionen des Models. Jetzt könnten Sie die neue Version des Models nach Belieben verändern. Die aktuelle Version wird festgelegt, indem in Xcode ein Model selektiert und im Menü der Menüpunkt Design | Data Model | Set Current Version ausgewählt wird. Neben einem versionierten Model wird zur Datenmigration noch ein Mappingmodel benötigt. Ein Mappingmodel beschreibt, wie Daten von einer beliebigen Version eines Models zu einer beliebigen Version eines anderen Models migriert werden.
Mappingmodels Ein Mappingmodel verbindet zwei unterschiedliche Versionen eines Models. Um ein Mappingmodel zu erzeugen, wählen Sie im Menü von Xcode den Menüpunkt File | New File... aus. Anschließend erscheint ein Fenster, welches nach der Art der zu erzeugenden Datei fragt. Wählen Sie das Template Mapping Model aus. Anschließend haben Sie die Möglichkeit, dem Mappingmodel einen Namen zuzuweisen. Es empfiehlt sich, den Mappingmodels Namen nach dem Schema VersionXto-VersionY zu geben, so dass gleich ersichtlich wird, was das Mappingmodel beschreibt. Im letzten Schritt muss noch ein Quellmodel und ein Zielmodel festgelegt werden. Das Quellmodel ist die ursprüngliche Version eines Models, und das Zielmodel ist die neue Version eines Models. Anschließend erscheint das Mappingmodel in Xcode. Xcode bietet eine grafische Oberfläche, um das Mappingmodel zu konfigurieren.
Xcode bietet eine grafische Oberfläche zur Konfiguration von Mappingmodels.
777
SmartBooks
Objective-C und Cocoa – Band 2
Wie Sie mit Xcode Mappingmodels konfigurieren, erklären wir Ihnen später. Zunächst möchten wir Ihnen die Klassen vorstellen, die die Datenmigration möglich machen:
Die Mappingklassen
NSMappingModel Instanzen der Klasse NSMappingModel beschreiben ein Mappingmodel. Durch ein Mappingmodel wird festgelegt, wie Daten einer bestimmten Version eines Models zu einer anderen Version eines Models migriert werden. Ein bereits vorhandenes Mappingmodel kann mit der Klassenmethode +mappingModelFromBundles:forSourceModel:destinationModel: von NSMappingModel geladen werden. Als erstes Argument erwartet diese Methode ein NSArray, welches Instanzen der Klasse NSBundle enthält, in denen nach Mappingmodels gesucht werden soll. Das zweite Argument muss eine Instanz von NSManagedObjectModel sein. Diese Instanz legt das Quellmodel fest. Das dritte Argument muss ebenfalls eine Instanz von NSManagedObjectModel sein, welche das Zielmodel festlegt. Die Klassenmethode +mappingModelFromBundles:forSourceModel:destinationModel: sucht dann in den festgelegten Bundles nach einem passenden Mappingmodel. Kann keines gefunden werden, so gibt diese Methode den Wert nil zurück. Andernfalls eine entsprechende Instanz von NSMappingModel. Ein NSMappingModel besteht wiederum aus Entitymappings. Jedes Mappingmodel verfügt über die Eigenschaft -entityMappings, welche ein NSArray liefert, das Instanzen der Klasse NSEntityMapping enthält. NSEntityMapping Eine Instanz der Klasse NSEntityMapping legt fest, wie eine Entität des Quellmodels auf eine Entität des Zielmodels abgebildet wird. Jedes NSEntityMapping muss über einen Namen verfügen, der mittels -setName: festgelegt wird. Mit -setSource EntityName: kann der Name der Entität des Quellmodels festgelegt werden. Das Gegenstück dazu ist die Methode -setDestinationEntityName:, mit der der Name der Entität des Zielmodels festgelegt werden kann. Ein Entitymapping muss nicht notwendigerweise über eine Zielentität und über eine Quellentität verfügen. Falls zum Beispiel eine Entität nur im Zielmodel existiert, so kann logischerweise der Name der Quellentität nicht angegeben werden. Wird andererseits im Zielmodel eine Entität ersatzlos gelöscht, so wird der Name der Zielentität im Entitymapping nicht angegeben. Die möglichen Konstellationen werden durch die Eigenschaft -mappingType repräsentiert. Es existieren folgende Konstellationen:
778
Kapitel 9
Auslieferung, Versionierung, Konfiguration
Konstellation
resultierender Typ des Mappings
Beschreibung
Nur der Name der Quellentität wurde angegeben.
NSRemoveEntityMappingType
Instanzen der Quellentität werden nicht migriert.
Nur der Name der Zielentität wurde angegeben.
NSAddEntityMappingType
Die Zielentität existiert lediglich im Zielmodel.
Name der Zielentität ist gleich dem Namen der Quellentität und die Zielentität ist kompatibel zur Quellentität.
NSCopyEntityMappingType
Instanzen der Quellentität werden ohne Änderung zu Instanzen der Zielentität.
Name der Zielentität ist gleich dem Namen der Quellentität, und die Zielentität ist inkompatibel zur Quellentität.
NSTransformEntityMapping- Zwischen Instanzen Type der Quellentität und Instanzen der Zielentität findet ein Mapping statt.
Weder der Name der Zielentität noch der Name der Quellentität wurde angegeben.
NSUndefinedEntityMapping- Es bleibt dem EntwickType ler überlassen, was genau geschieht.
Eine beliebige, bereits NSCustomEntityMappingbeschriebene KonsType tellation wird mit der Angabe einer »Custom Policy« kombiniert.
Die angegebene »Custom Policy« (Name einer Subklasse von NSEntityMigrationPolicy) übernimmt die Migration.
Zusätzlich zu Name und Typ verfügen Entitymappings über Relationshipmappings und über Attributemappings. Mit der Methode -setAttributeMappings: können die Attributemappings und mit der Methode -setRelationshipMappings: können die Relationshipmappings festgelegt werden. Beide Methoden erwarten ein NSArray, welches mit Instanzen der Klasse NSPropertyMapping gefüllt ist. NSPropertyMapping Instanzen der Klasse NSPropertyMapping beschreiben, wie eine Eigenschaft einer Quellentität auf eine Eigenschaft der Zielentität abgebildet wird. Jedes Proper779
SmartBooks
Objective-C und Cocoa – Band 2
tymapping muss über einen Namen verfügen, der mit -setName: festgelegt wird. Mittels -setValueExpression: wird ein Ausdruck angegeben, der die Abbildung des Propertymappings genauer beschreibt. Der Mappingeditor von Xcode erzeugt beim Anlegen eines Mappingmodels nach bestem Wissen Propertymappings und Entitymappings. Propertymappings können übrigens Attributemappings sowie Relationshipmappings repräsentieren. NSEntityMigrationPolicy Entitymappings verfügen über die Methode -setEntityMigrationPolicyClassName:. Diese Methode legt den Namen einer Subklasse von NSEntityMigrationPolicy fest. Komplexere Entitymappings benötigen spezielle Instanzen der Klasse NSEntityMigrationPolicy. Der Name einer Subklasse von NSEntityMigrationPolicy kann im Mappingeditor von Xcode angegeben werden. In einer Subklasse von NSEntity MigrationPolicy haben Sie die Möglichkeit, Zielobjekte selbst zu erzeugen und deren Attribute und Beziehungen nach eigenem Belieben zu setzen. Wir werden im weiteren Verlauf dieses Kapitels eine Subklasse von NSEntityMigrationPolicy erzeugen. NSMigrationManager Mit einem NSMigrationManager wird eine Migration gestartet und durchgeführt. Sie werden nur dann einen Migrationmanager erzeugen müssen, wenn Sie die Migration selbst starten möchten. Core Data ist in der Lage, die Notwendigkeit einer Migration zu erkennen, um dann eigenständig einen Migrationmanager zu erzeugen. Standardmäßig ist dieser Automatismus nicht aktiv. Beim Hinzufügen eines persistenten Stores (NSPersistentStore) zu einem Storecoordinator (NSPersistent StoreCoordinator) kann dieser Automatismus aktiviert werden. Die Methode -addPersistentStoreWithType:configuration:URL:options:error: der Klasse NSPersistentStoreCoordinator erwartet als viertes Argument ein NSDictionary, welches die Optionen des anzulegenden persistenten Stores enthält. Falls dieses Dictionary den Schlüssel NSMigratePersistentStoresAutomaticallyOption und als Wert eine Boolsche NSNumber-Instanz mit YES enthält, so wird Core Data versuchen, Migrationen automatisch durchzuführen. Bereits mit dem Automatismus von Core Data lassen sich sehr interessante und anspruchsvolle Migrationen durchführen, was wir jetzt auch anhand eines Beispiels zeigen möchten.
Automatische Migration Um eine Datenmigration durchführen zu können, werden zumindest zwei unterschiedliche Versionen eines Models benötigt. Zusätzlich wird noch ein Mappingmodel benötigt. Wir möchten jetzt anhand eines kleinen Beispiels eine automatische Migration durchführen. Hierzu benötigen wir eine Core Data-Anwendung. Die im 780
Kapitel 9
Auslieferung, Versionierung, Konfiguration
Rahmen dieses Beispiels betrachtete Anwendung verwaltet Häuser und Personen. Eine Person hat einen Namen und ein gewisses Alter. Ein Haus hat eine Hausnummer, einen Straßennamen sowie den Namen einer Stadt, in der das Haus steht. Zusätzlich dazu hat ein Haus einen eindeutigen Identifier. In jedem Haus können mehrere Personen leben, und eine Person kann genau einem Haus zugeordnet sein.
Person NSString name NSNumber age house
House NSString city NSString identifier NSString street NSNumber houseNumber persons
Das Datenmodel der ersten Version der Anwendung: Zwischen der Entität Haus und der Entität Person besteht eine 1:n Beziehung.
Die Anwendung ist eine Core Data-Dokumentenanwendung mit entsprechender Benutzerschnittstelle, die das Anlegen von Häusern und Personen erlaubt. Wir ersparen es Ihnen, die erste Version der Anwendung selbst zu erstellen. Sie würden nichts Neues lernen. Daher bitten wir Sie, sich die erste Version der Anwendung einfach von der Webseite des Buches herunterladen. Sie finden die erste Version der Anwendung unter dem Namen Persons1. Die Anwendung enthält bereits das Datenmodel und eine Benutzerschnittstelle. Bei der Erläuterung der Klasse NSMigrationManager haben wir erwähnt, dass Core Data in der Lage ist, Migrationen eigenständig anzuwerfen und dass dieser Automatismus standardmäßig nicht aktiviert ist. Dieser Automatismus wird in der Datei MyDocument.m aktiviert. Öffnen Sie bitte die Datei MyDocument.m. Dort wird die Methode –configurePersistentStoreCoordinatorForURL:ofType:modelConfiguration:storeOptions:error: überschrieben. Diese Methode gibt uns Gelegenheit, die Optionen des Stores anzupassen, um so den Automatismus zu aktivieren. - (BOOL)configurePersistentStoreCoordinatorForURL:(NSURL*)url ofType:(NSString*)fileType modelConfiguration:(NSString*)configuration storeOptions:(NSDictionary*)storeOptions error:(NSError**)error { NSMutableDictionary* newOp = [NSMutableDictionary dictionary]; [newOp addEntriesFromDictionary:storeOptions]; [newOp setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];
781
SmartBooks
Objective-C und Cocoa – Band 2
return [super configurePersistentStoreCoordinatorForURL:url ofType:fileType modelConfiguration:configuration storeOptions:newOp error:error]; }
In allen künftigen Versionen der Anwendung wird die Methode –configurePersistentStoreCoordinatorForURL:ofType:modelConfiguration:storeOptions:error: in gleicher Weise überschrieben. Starten Sie die Anwendung und erzeugen Sie einige Häuser und auch einige Personen. Speichern Sie das Dokument dann ab und wählen Sie als Dateiname »Persons1«. Wir befinden uns jetzt also in dem Zustand, dass wir eine erste Version unserer Anwendung haben.
Erzeugen eines versionierten Models sowie eines Mappingmodels Nun soll die Anwendung Persons1 (aus dem vorherigem Abschnitt) weiterentwickelt werden. Mit der zweiten Version der Anwendung, die ein anderes Datenmodel haben wird, werden Sie dann die Datei Persons1 öffnen, und Core Data wird den Migrationsprozess automatisch starten.
HILFE Sie können das Projekt aus dem vorherigen Abschnitt als »Persons1« von der Webseite herunterladen. Erzeugen Sie dazu eine Kopie des Ordners, der das Projekt »Persons1« enthält. Ändern Sie den Namen des Ordners auf »Persons2«. Die zweite Version der Personenanwendung hat ein leicht abgewandeltes Datenmodel. Person NSString name NSNumber age house
House NSString identifier persons address
Address NSString city NSString street NSNumber houseNumber house
Das Datenmodel der zweiten Version der Personenanwendung
Das neue Datenmodel verfügt über eine neue Entität. Diese Entität heißt Address und repräsentiert eine Adresse eines Hauses. Die Attribute city, street und houseNumber befinden sich nun nicht mehr in der Entität House, sondern in der Entität Address. Zwischen den Entitäten Address und House existiert eine 1:1 Beziehung, was bedeutet, dass ein Haus genau über eine Adresse verfügt. Die Adressinforma782
Kapitel 9
Auslieferung, Versionierung, Konfiguration
tionen eines Hauses werden in der zweiten Version des Datenmodels folglich in die Entität Address ausgelagert. Selektieren Sie nun in Xcode das bisherige Datenmodel (MyDocument.xcdatamodel) und wählen Sie dann im Menü den Menüpunkt Design | Data Model | Add Model Version aus. Xcode erzeugt für Sie ein versioniertes Datenmodel (MyDocument.xcdatamodeld) und eine Kopie (MyDocument 2.xcdatamodel) des bisherigen Datenmodels. Markieren Sie jetzt bitte das MyDocument 2.xcdatamodel-Datenmodel in der Projektleiste und wählen Sie im Menü den Menüpunkt Design | Data Model | Set Current Version aus. Ab jetzt erwartet die Anwendung Daten im neuen Format. Falls mit der Anwendung Daten im alten Format geöffnet werden, so würde Core Data versuchen, ein Mapping zu starten. Passen Sie jetzt bitte das Datenmodel MyDocument 2.xcdatamodel entsprechend der obigen Abbildung an. Anschließend öffnen Sie die Datei MyDocument.xib und nehmen Sie die notwendigen Änderungen vor, so dass die Benutzerschnittstelle der folgenden Abbildung entspricht. Binding: Value Bind to: Houses Array Controller Controller Key: selection Model Key Path: address.houseNumber
Binding: Value Bind to: Houses Array Controller Controller Key: selection Model Key Path: address.city Binding: Value Bind to: Houses Array Controller Controller Key: selection Model Key Path: address.street
Die Benutzerschnittstelle der zweiten Version der Beispielanwendung
Jetzt haben wir alles beisammen, um mit der Definition des Mappingmodels beginnen zu können. Erzeugen Sie mittels Menüpunkt File | New File... ein Mappingmodel, welches Sie bitte Persons1-to-Persons2 nennen. Als Quellmodel wählen Sie MyDocument.xcdatamodel und als Zielmodel MyDocument 2.xcdatamodel aus. Xcode sollte 783
SmartBooks
Objective-C und Cocoa – Band 2
für Sie eine Datei namens Persons1-to-Persons2.xcmappingmodel erzeugt haben. Diese Datei ist das Mappingmodel und beschreibt, wie Daten, die mit der ersten Version der Anwendung erzeugt wurden, in das Format der zweiten Version konvertiert werden. Machen Sie einen Doppelklick auf das Mappingmodel. Xcode öffnet den Editor zur Bearbeitung von Mappingmodels. Die Tabelle im linken Bereich enthält die Entitymappings. Wie Sie sehen, hat Xcode schon drei Entitymappings angelegt. In der mittleren Tabelle werden die Propertymappings angezeigt, die sich im ausgewählten Entitymapping befinden. In der Spalte mit dem Titel T wird der jeweilige Typ des Propertymappings angezeigt. Der Wert R steht für Relationshipmapping, wohingegen der Wert A für Attributemapping steht. Die rechte Spalte enthält weitere Informationen zum aktuell ausgewählten Objekt. Ist ein Entitymapping ausgewählt, so zeigt die dritte Spalte weitere Informationen des Entitymappings an. Ist ein Propertymapping ausgewählt, so zeigt die dritte Spalte weitere Informationen des Propertymappings an. Lassen Sie uns nun die von Xcode automatisch erzeugten Entitymappings und Propertymappings genauer anschauen. Wählen Sie das Entitymapping namens PersonToPerson aus. Der Name legt nahe, dass dieses Entitymapping beschreibt, wie Personen von der ersten auf die zweite Version abgebildet werden. Die rechte Spalte bestätigt diese Vermutung. Die Quellentität (Source) entspricht der Zielentität (Destination). Der Typ des Entitymappings hat den Wert Copy. Das Entitymapping PersonToPerson enthält drei Propertymappings. Das Propertymapping age hat den Ausdruck (Value Expression) $source.age. Für den Moment genügt es, die Auswirkung dieses Ausdrucks zu verstehen. Das Alter einer Person im neuen Format entspricht dem Wert des Attributs age einer Person im alten Format. Den genauen Ablauf einer Migration erklären wir Ihnen etwas später. Der Name einer »neuen« Person wird ebenfalls aus den Namen der entsprechenden »alten« Person gewonnen. Wesentlich komplizierter ist der Ausdruck des Relationshipmappings house. Der Ausdruck bewirkt, dass das Haus einer Person im »alten Format« gleich dem Haus einer Person im »neuen Format« ist. Später werden wir erklären, wie der Ausdruck zustande kommt. Das Entitymapping namens HouseToHouse beschreibt, wie Häuser abgebildet werden. Der Typ dieses Entitymappings ist Transform, da die Entität House in der zweiten Version des Models geändert wurde. Das Entitymapping namens Address verfügt lediglich über eine Zielentität. Daher ist der Typ des Entitymappings Add. Xcode hat also schon ganz schön viel Arbeit geleistet. Das Mappingmodel ist fast komplett. Ihm fehlt lediglich noch ein Entitymapping, welches die Adressdaten aus der Entität House extrahiert und in die neue Entität Address einfügt. 784
Kapitel 9
Auslieferung, Versionierung, Konfiguration
Erzeugen Sie ein neues Entitymapping, indem Sie links unten auf den Button mit dem Pluszeichen klicken. Nennen Sie das neue Entitymapping HouseToAddress. Wählen Sie als Quellentität House und als Zielentität Address aus. Markieren Sie das neu angelegte Entitymapping und erzeugen Sie drei Attributemappings. Das erste nennen Sie city und versehen dieses mit dem Ausdruck $source.city. Das zweite nennen Sie houseNumber und versehen es mit dem Ausdruck $source.houseNumber. Das dritte nennen Sie street und versehen es mit dem Ausdruck $source.street. Nun ist das Mapping komplett. Kompilieren Sie die Anwendung und öffnen Sie mit dieser die Datei, die Sie mit der ersten Version der Anwendung angelegt haben (Persons1). Dies sollte ohne Probleme klappen, obwohl die Datei Persons1 ein inkompatibles Datenformat hat. Core Data erkennt diesen Konflikt und initiiert automatisch eine Migration.
HILFE Sie können das Projekt in diesem Zustand als »Persons2« von der Webseite herunterladen.
Der Migrationsprozess Für die Durchführung einer Migration ist eine Instanz der Klasse NSMigrationManager notwendig. Ein Migrationmanager wird entweder automatisch von Core Data oder manuell durch den Entwickler erzeugt. Die Methode -migrateStoreFrom URL:type:options:withMappingModel:toDestinationURL:destinationType:destinationOptions:error: startet dann die Migration. Hierbei werden automatisch zwei komplette Core Data-Stacks (Band I, S. 605 ff.) erzeugt, jeweils einer für das Quellmodel und einer für das Zielmodel. Die eigentliche Migration ist ein dreistufiger Prozess: Erste Stufe: Zielobjekte erzeugen Core Data wirft einen Blick in das Mappingmodel des Migrationmanagers und arbeitet nacheinander alle darin enthaltenen Entitymappings ab. Dabei erzeugt Core Data im Stack des Quellmodels Quellobjekte. Für jedes Quellobjekt ruft Core Data die Methode –createDestinationInstancesForSourceInstance:entityMapping:manager:error: der Klasse NSEntityMigrationPolicy auf. Falls Sie (im Mappingeditor) bei einem oder mehreren Entitymapping(s) eine Custom-Policy angegeben haben, so wird die Methode –createDestinationInstancesForSourceInstance:entityMapping:manager:error: auf eine Instanz der abgegebenen Custom-Policy-Klasse angewandt. Dieser Methode wird ein Quellobjekt übergeben. Für dieses Quellobjekt werden kein, ein oder mehrere Zielobjekte erzeugt. Für jedes erzeugte Zielobjekt muss innerhalb dieser Methode eine Verbindung zum Quellobjekt hergestellt werden. Dies geschieht mittels der 785
SmartBooks
Objective-C und Cocoa – Band 2
Methode -associateSourceInstance:withDestinationInstance:forEntityMapping: von NSMigrationManager. Die erste Stufe erzeugt folglich Zielobjekte anhand von Quellobjekten und versieht deren Attribute mit Werten. Im weiteren Verlauf dieses Kapitels werden wir eine Subklasse von NSEntityMigrationPolicy teilweise implementieren. Zweite Stufe: Beziehungen erzeugen In der zweiten Stufe sendet Core Data an jede NSEntityMigrationPolicy eines Entitymappings die Nachricht –createRelationshipsForDestinationInstance:entityMapping:manager:error:. Diese Methode erzeugt die Beziehungen eines Zielobjektes. Dritte Stufe: Überprüfung der Gültigkeit und Sicherung des Zielstores Die letzte Stufe kann zur Überprüfung der Gültigkeit der Zielobjekte genutzt werden. Sie fragen sich vielleicht, wieso die Gültigkeit nach einer Migration nicht gegeben sein kann. Vor der ersten Stufe werden zwei Core Data-Stacks erzeugt. Im Zielstack werden die Validierungsregeln (siehe -validationPredicates von NSPropertyDescription) deaktiviert. Dies ist sinnvoll, da ja bei der Migration ungültige Zwischenzustände entstehen können. Bleiben diese bis zum Abschluss bestehen, so fällt dies erst hier auf.
Funktionsweise von Propertymappings Ein Mappingmodel besteht aus mehreren Entitymappings und ein Entitymapping besteht aus mehreren Propertymappings. Propertymappings legen fest, wie Eigenschaften von einem Quellmodel auf ein Zielmodel abgebildet werden. Aber wie genau funktioniert das? Ein Propertymapping verfügt im Wesentlichen nur über einen Namen (-name) und über einen Ausdruck (-valueExpression). Als Sie im Rahmen des ersten Beispiels das Mappingmodel in Xcode vor sich hatten, sind Ihnen bestimmt die kompliziert aussehenden Ausdrücke der Relationshipmappings aufgefallen. Nehmen wir als Beispiel das Relationshipmapping namens house aus dem Entitymapping PersonToPerson. Dieses Relationshipmapping (ein Relation shipmapping ist nichts weiter als ein Propertymapping mit einem »speziellen« Ausdruck) weist einer Person ihr entsprechendes Haus zu. Der Ausdruck, der von Xcode hierfür erzeugt wurde, lautet wie folgt: FUNCTION( $manager, "destinationInstancesForEntityMappingNamed:sourceInstances:", "HouseToHouse", $source.house )
Ausdrücke werden von der Klasse NSExpression repräsentiert. Seit OS X 10.5 ist es möglich, mit NSExpression sogenannte Funktionsausdrücke (Function Expressions) zu erzeugen. Die Syntax hierfür lautet FUNCTION(Empfänger, Name eines 786
Kapitel 9
Auslieferung, Versionierung, Konfiguration
Selectors, Argumente, ...). Nun betrachten wir dies im Zusammenhang mit der obigen Funktion. Der Empfänger, $manager, wird bei der Ausführung des Ausdruckes durch eine Instanz von NSMigrationManager ersetzt. Da der Name des Selectors destinationInstancesForEntityMappingNamed:sourceInstances: ist, wird an den Migrationmanager die Nachricht –destinationInstancesForEntityMapping Named:sourceInstances: geschickt. Der erste Parameter ist der String HouseTo House und der zweite Parameter ist $source.house. Die Methode –destinationInstancesForEntityMappingNamed:sourceInstances: ermittelt anhand des Namens eines Entitymappings und einem Array von Quellobjekten die dazu passenden Zielobjekte. Der Ausdruck eines Propertymappings wird dazu benutzt, um den Zielwert (im Falle eines Attributemappings) oder um die Zielobjekte (im Falle eines Relationshipmappings) zu ermitteln. Das ist genau das, was von einem Propertymapping erwartet wird. Sie sollten sich nun auch die anderen Ausdrücke erklären können.
Eigene NSEntityMigrationPolicy Mit einer Subklasse von NSEntityMigrationPolicy kann der Migrationsprozess eines Entitymappings an die eigenen Bedürfnisse angepasst werden. Vielleicht haben Sie eine Änderung an Ihrem Model vorgenommen, die nicht durch ein Entitymapping abgebildet werden kann. Eine solche Änderung des Models wurde bereits in der Einleitung angesprochen. Stellen Sie sich vor, dass das Attribut namens name aus dem vorherigen Beispiel durch die Attribute firstName und lastName ersetzt wird. Es ist nicht möglich, eine solche Änderung alleine durch ein Entitymapping zu beschreiben. Eine eigene Migrationpolicy muss her! Wir werden nun eine dritte Version der Beispielanwendung entwickeln und nochmals das Datenmodel ändern.
HILFE Sie können das Projekt aus dem vorherigen Abschnitt als »Persons2« von der Webseite herunterladen. Erstellen Sie eine Kopie des Ordners Persons2. Nennen Sie diese Kopie bitte »Persons3«. Öffnen Sie das darin enthaltene Projekt mit Xcode. Löschen Sie das Model namens MyDocument.xcdatamodel. Markieren Sie anschließend das Datenmodel namens MyDocument 2.xcdatamodel und wählen Sie im Menü den Menüpunkt Design | Data Model | Add Model Version aus. Es sollte in der Projektleiste ein Model namens MyDocument 3.xcdatamodel erscheinen. Markieren Sie dieses Model und wählen Sie den Menüpunkt Design | Data Model | Set Cur-
787
SmartBooks
Objective-C und Cocoa – Band 2
rent Version aus. Passen Sie dieses Model an, so dass es der folgenden Abbildung entspricht. Person NSString firstName NSString lastName NSNumber age house
House NSString identifier persons address
Address NSString city NSString street NSNumber houseNumber house
Das Datenmodel der dritten Version der Personenanwendung
Löschen Sie anschließend das Mappingmodel. Legen Sie nun ein neues Mappingmodel an. Nennen Sie dieses Mappingmodel Persons2-to-Persons3 und wählen Sie als Quellmodel MyDocument 2.xcdatamodel und als Zielmodel MyDocument 3.xcdatamodel. Das von Xcode erzeugte Mappingmodel entspricht, bis auf eine kleine Ausnahme, dem, was wir möchten. Sie müssen lediglich die Custom-Policy des Entitymappings PersonToPerson auf PersonToPersonPolicy setzen. Hiermit teilen Sie Core Data mit, dass eine entsprechende Klasse PersonToPersonPolicy existiert und dass diese im Rahmen des dreistufigen Migrationsprozesses beim Entitymapping PersonToPerson hier und da ein Wörtchen mitreden möchte. Die Klasse PersonToPersonPolicy existiert noch nicht. Sie müssen diese erzeugen. Wählen Sie den Menüpunkt File | New File... aus und erstellen Sie eine Subklasse von NSObject. Nennen Sie die Klasse PersonToPersonPolicy. Öffnen Sie die Datei PersonToPersonPolicy.h und ändern Sie die Superklasse auf NSEntityMigrationPolicy. Wechseln Sie anschließend zu Datei PersonToPersonPolicy.m. An dieser Stelle können Sie nun jede Methode von NSEntityMigrationPolicy überschreiben und so das Verhalten des Entitymappings PersonToPerson anpassen. Das Ziel ist es, aus einem Namen wie »Christian Kienle« den Vornamen und den Nachnamen zu erhalten. Der Einfachheit halber gehen wir davon aus, dass jeder Name genau aus zwei Worten besteht und dass das erste Wort dem Vornamen und das zweite Wort dem Nachnamen entspricht. Da die PersonToPersonPolicy also lediglich Werte von Attributen ändert, klinken wir uns in die erste Stufe des Migrationsprozesses ein. Dies bedeutet, dass die Methode –createDestinationInstancesForSourceInstance:entityMapping:manager:error: überschrieben werden muss.
788
Kapitel 9
Auslieferung, Versionierung, Konfiguration
- (BOOL)createDestinationInstancesForSourceInstance: (NSManagedObject*)source entityMapping:(NSEntityMapping*)mapping manager:(NSMigrationManager*)manager error:(NSError**)error { NSManagedObjectContext* ctx = [manager destinationContext]; id person = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:ctx]; NSString *name = [source valueForKey:@"name"]; NSArray *names = [name componentsSeparatedByString:@" "]; NSString *firstName = [names objectAtIndex:0]; NSString *lastName = [names lastObject]; NSNumber *age = [source valueForKey:@"age"]; [person setValue:firstName forKey:@"firstName"]; [person setValue:lastName forKey:@"lastName"]; [person setValue:age forKey:@"age"]; [manager associateSourceInstance:source withDestinationInstance:person forEntityMapping:mapping]; return YES; }
Innerhalb dieser Methode wird zunächst eine neue Person erzeugt. Anschließend wird der Name des Quellobjektes ermittelt und in Vorname und Nachname aufgeteilt. Im vorletzten Schritt wird die neu erzeugte Person über ihren Vornamen, Nachnamen und über ihr Alter informiert. Zum Schluss wird dem Migrationmanager mitgeteilt, welche Zielobjekte anhand des Quellobjekts erzeugt wurden. Jetzt müssen Sie noch die Tabelle anpassen, die die Personen anzeigt. Öffnen Sie hierzu die Datei MyDocument.xib und löschen Sie aus der Personentabelle die Spalte für den Namen. Anschließend fügen Sie zwei neue Spalten ein. Eine für den Vornamen und eine für den Nachnamen.
789
SmartBooks
Objective-C und Cocoa – Band 2
Binding: Value Bind to: Persons Array Controller Controller Key: arrangedObjects Model Key Path: lastName
Binding: Value Bind to: Persons Array Controller Controller Key: arrangedObjects Model Key Path: firstName
Die Benutzerschnittstelle der dritten Version der Beispielanwendung. Sie müssen lediglich die Bindings der neuen Spalten anpassen. Der Rest verändert sich nicht.
Speichern Sie alles ab, kompilieren und starten Sie die Anwendung. Versuchen Sie nun, eine Datei zu öffnen, die Sie mit der zweiten Version der Beispielanwendung erzeugt haben.
HILFE Sie können das Projekt aus dem vorherigen Abschnitt als »Persons3« von der Webseite herunterladen.
Manuelle Migration Die vorgestellte Beispielanwendung bediente sich dem von Core Data bereitgestellten Automatismus zur Durchführung einer Migration. Dieser Automatismus muss folglich einen Kompatibilitätskonflikt erkennen können, um dann einen entsprechenden Migrationsprozess anzustoßen. 790
Kapitel 9
Auslieferung, Versionierung, Konfiguration
Kompatibilitätskonflikte erkennen Jetzt stellt sich natürlich die Frage, wie Core Data Kompatibilitätskonflikte erkennt. Core Data ist schlau und bildet aus den für die Persistenz wichtigen Eigenschaften eines Models einen Hash. Beim Speichern schreibt Core Data seit OS X 10.5 diesen Hash in die Metadaten des Stores. Anhand dieses Hashs ist Core Data dann in der Lage, Konflikte zu erkennen. Dies ist auch der Schlüssel, um selbst festzustellen, ob zwei Models kompatibel zueinander sind. Die Klassenmethode +metadataForPersistentStoreOfType:URL:error: von NSPersistentStoreCoordinator liefert die Metadaten eines bestimmten Stores. Anhand dieser Metadaten und einem NS ManagedObjectModel kann dann mit der Methode -isConfiguration:compatible WithStoreMetadata: von NSManagedObjectModel die Kompatibilität oder Inkompatibilität festgestellt werden.
Anstoßen eines Migrationsprozesses Um selbst einen Migrationsprozess anzustoßen, werden zunächst zwei Instanzen von NSManagedObjectModel benötigt. Zum einen das Quellmodel und zum anderen das Zielmodel. Mit den Models kann dann unter Verwendung der Methode -initWithSourceModel:destinationModel: ein NSMigrationManager erzeugt werden. Anschließend wird mittels der Klassenmethode +mappingModelFromBundles:forSourceModel:destinationModel: ein NSMappingModel erzeugt. Die Migration wird dann mit der Methode -migrateStoreFromURL:type:options:withMappingModel:toDestinationURL:destinationType:destinationOptions:error: angeworfen. Wie Sie sehen, gestaltet sich die manuelle Migration ebenfalls sehr einfach. Aber solange der von Core Data bereitgestellte Mechanismus für Ihre Vorhaben ausreichend funktioniert, gibt es eigentlich keinen Grund, selbst Hand anzulegen.
Fazit Die Versionierungsmöglichkeiten von Core Data sind einfach, pragmatisch und flexibel. Der Migrationsprozess kann komplett in die Hände von Core Data gelegt oder aber selbst vorgenommen werden. Xcode unterstützt Sie bei der Versionierung von Datenmodels und auch bei der Erzeugung von Mappingmodels. Aus zwei gegebenen Datenmodels versucht Core Data ein möglichst komplettes Mappingmodel zu erzeugen, welches lediglich geringfügiger Anpassungen bedarf.
Frameworks Ein Framework ist eine Sammlung von Klassen und Resourcen, die von außen betrachtet eine Einheit bilden. Sie kennen ja bereits das Framework Cocoa, wel791
SmartBooks
Objective-C und Cocoa – Band 2
ches sich wiederum aus den Frameworks Foundation, AppKit und Core Data zusammensetzt. Betrachten wir als Beispiel das Core Data-Framework. Apple hat mit Core Data eine Sammlung von Klassen und Resourcen geschaffen, die das Problem der Datenverwaltung löst. Hierbei ist die Lösung des Problems recht generisch gehalten. Dies bedeutet wiederum, dass Entwickler Core Data erweitern können, so dass das eigene, spezielle Vorhaben realisiert werden kann. Sie können auch selbst solche Frameworks erstellen. Dies hat dann den Vorteil, dass Sie gemeinsamen Code verschiedener Anwendungen oder verschiedener Versionen einer Anwendung bündeln können und nicht doppelt warten müssen.
Einsatzgebiete Wir wollen in diesem Abschnitt die Frage klären, wann es Sinn macht, ein eigenes Framework zu schaffen, und wie man hierbei vorgeht. Denn so schön Abstraktion auch ist, man sollte es nicht übertreiben. Gerade als Anwendungsentwickler sollten gute Gründe vorliegen, um Zeit in ein eigenes Framework zu investieren. Falls Sie in zwei oder mehr Anwendungen ähnliche Funktionalität benötigen, so können Sie diese in ein Framework auslagern. Erfahrungsgemäß ist es sinnvoll, zunächst mindestens zweimal eine ähnliche Funktionalität in zwei unterschiedlichen Anwendungen zu implementieren und anschließend die Gemeinsamkeiten in ein Framework auszulagern. Wenn Sie sich daran halten, so haben Sie es viel einfacher, da Sie die Gemeinsamkeiten sofort sehen. Halten Sie sich nicht an diese Regel, so ist das Risiko recht hoch, dass Sie Dinge in ein Framework auslagern, die dann doch nur von einer Anwendung benötigt werden. Dies ist dann gerne vergebliche Liebesmüh. Allerdings kann dies auch eine Möglichkeit zur Kapselung einer festen Grundlage sein. Da Ihr Framework bei dieser Vorgehensweise weniger umfangreich und sich auf die gemeinsame Funktionalität reduziert, sparen Sie sich viel Arbeit. Es ist nämlich sehr anspruchsvoll und arbeitsintensiv, ein gutes Framework zu entwickeln und es zu warten. Stellen Sie sich vor, dass Sie ein eigenes Framework entwickelten und es nun in zwei Anwendungen nutzen. Nun kommt eine dritte Anwendung dazu, in der Sie gerne die Vorteile der Garbage-Collection nutzen möchten. Dies ist in der Regel kein Problem, da fast alle Frameworks, die von Apple kommen, explizit die Garbage-Collection unterstützen. Ihr eigenes Framework muss von Ihnen dann zunächst explizit für die Garbage-Collection angepasst (Band I, S. 274 f.) und getestet werden. Durch eigene Frameworks verlieren Sie folglich erstmal einiges an Flexibilität, obwohl Sie ja eigentlich mit dem Framework flexibler werden wollten.
792
Kapitel 9
Auslieferung, Versionierung, Konfiguration
Framework erstellen Sie haben festgestellt, dass es für Sie sinnvoll wäre, ein eigenes Framework zu schaffen, und möchten keine weitere Zeit verlieren: Erzeugen Sie ein neues Projekt vom Typ Cocoa Framework.
Erstellen eines CocoaFrameworks
Nennen Sie das Projekt MyFramework. Das Framework sollte sich sofort kompilieren lassen. Allerdings haben Sie keine Möglichkeit, das Framework wie eine Anwendung zu starten. Dies liegt daran, dass ein Framework keine Anwendungslogik enthält. Davon abgesehen gehen Sie mit einem Framework um wie auch mit einer Anwendung. Klassen erzeugen, andere Frameworks linken, Resourcen hinzufügen und Nibs anlegen ist für Sie folglich alter Kaffee. Lassen Sie uns dies ausprobieren. Legen Sie eine neue Klasse an und nennen Sie diese AlertGenerator. Die Klasse AlertGenerator soll über eine einzige Methode verfügen, die Sie bitte -displayAlert nennen. Der Inhalt von AlertGenerator.h sollte wie folgt aussehen: #import @interface AlertGenerator : NSObject { } - (void)displayAlert; @end
Beim Empfang der Nachricht -displayAlert soll ein Alert-Panel angezeigt werden. Hierzu bedienen wir uns der Funktion NSRunInformationalAlertPanel. 793
SmartBooks
Objective-C und Cocoa – Band 2
#import "AlertGenerator.h" @implementation AlertGenerator - (void)displayAlert { NSRunInformationalAlertPanel(@"Hello World", @"", nil, nil, nil); } @end
Das Framework enthält jetzt eine Klasse, AlertGenerator, und eine Methode -displayAlert. Bevor das Framework von einer Anwendung genutzt werden kann, müssen noch zwei Dinge erledigt werden. Ein Cocoa-Framework ist nichts weiter als ein Ordner, der mit der Endung .framework versehen ist. Außerdem hat der Ordner eine spezielle Struktur, die automatisch von Xcode erzeugt wird, sobald ein Framework kompiliert wird. In dieser internen Struktur sollten auch die Header-Dateien des Frameworks abgelegt werden. Standardmäßig geschieht dies aber nicht. Um dies zu ändern, wählen Sie in Xcode auf der linken Seite unter Targets Ihr Framework, MyFramework, aus. Auf der rechten Seite sollten dann alle Dateien erscheinen, die zu Ihrem Framework gehören. Dort finden Sie auch die Dateien AlertGenerator.h und AlertGenerator.m. Außerdem gibt es in dieser Tabelle auch eine Spalte namens Role. Der Wert der Role von AlertGenerator.h steht standardmäßig auf project. Dies bedeutet, dass Sie die Datei innerhalb Ihres Frameworks nutzen können.
Die Role von project auf public ändern
Ändern Sie den Wert auf public. Falls Sie nun Ihr Projekt bereinigen (im Menü wählen Sie hierzu Build | Clean) und kompilieren, so legt Xcode die Datei AlertGenerator.h im Framework ab. Dies müssen Sie mit jeder Headerdatei machen, die von außerhalb des Frameworks nutzbar sein soll. Im letzten Schritt müssen Sie noch das Installationsverzeichnis korrekt angeben. Das Installationsverzeichnis 794
Kapitel 9
Auslieferung, Versionierung, Konfiguration
eines Frameworks legt den Ordner fest, in welchen das Framework kopiert wird, sobald es von einer Anwendung genutzt wird. Sie erinnern sich vielleicht, dass eine Anwendung auch nur ein Ordner ist, der - wie Frameworks auch - eine besondere Struktur hat. Im Unterordner Contents | Frameworks liegen die von einer Anwendung genutzten Frameworks. In dieses Verzeichnis soll folglich Ihr Framework kopiert werden, sobald die Anwendung kompiliert wird. Wählen Sie hierzu unter Targets den Eintrag MyFramework aus. Rufen dann durch einen Klick auf das InfoSymbol den Inspektor auf. Geben Sie in das Suchfeld instal ein. Stellen Sie anschließend in den Build-Informationen sicher, dass All Configurations und All Settings ausgewählt sind. Ändern Sie dann den Wert des Eintrages Installation Directory auf »@executable_path/../Frameworks«. Beim Kompilieren der Anwendung wird @executable_path durch den Pfad ersetzt, in dem das Binary der Anwendung liegt. Im Elternverzeichnis (..) des Binarys existiert ein Ordner namens Frameworks, in den das Framework kopiert werden soll. 2
3 5
4
6
1 7
Die sieben Schritte zum richtigen Installationsverzeichnis
HILFE Sie können das Projekt in diesem Zustand als »MyFramework 01« von der Webseite des Buches herunterladen.
795
SmartBooks
Objective-C und Cocoa – Band 2
Framework nutzen Beim Einbinden des Frameworks in die eigene Anwendung gibt es einige Kleinigkeiten zu beachten, auf die wir in diesem Abschnitt noch hinweisen möchten. Zum Nachvollziehen dieser Hinweise legen Sie sich am besten ein neues Projekt vom Typ Cocoa an. Nennen Sie dieses Projekt FrameworkConsumer. Im ersten Schritt müssen Sie das Projekt, welches Ihr Framework enthält, referenzieren. Ziehen Sie hierzu das Frameworkprojekt in den Groups & Files-Bereich des Projektes FrameworkConsumer.
Referenzieren des Frameworks
Bestätigen Sie den anschließend erscheinenden Dialog. Nun muss MyFramework noch als Abhängigkeit festgelegt werden. Dies hat zur Folge, dass MyFramework automatisch kompiliert wird. Wählen Sie hierzu das Target FrameworkConsumer aus und öffnen Sie das Infofenster. Unter General fügen Sie eine Direct Dependency hinzu. Wählen Sie MyFramework aus und bestätigen Sie das Sheet mit einem Klick auf Add Target. Im letzten Schritt legen Sie eine neue Copy Files Build Phase an. Bei Destination wählen Sie Frameworks aus. Ziehen Sie in diese Build-Phase die Datei MyFramework.framework. Nun können Sie das Framework nutzen. Öffnen Sie hierzu die Datei FrameworkConsumerAppDelegate.m und passen Sie den Inhalt wie folgt an. 796
Kapitel 9
Auslieferung, Versionierung, Konfiguration
#import "FrameworkConsumerAppDelegate.h" #import @implementation FrameworkConsumerAppDelegate @synthesize window; - (void)applicationDidFinishLaunching:(NSNotification*)n { AlertGenerator*a = [[[AlertGenerator alloc] init] autorelease]; [a displayAlert]; } @end
HILFE Sie können das Projekt in diesem Zustand als »FrameworkConsumer 1« von der Webseite des Buches herunterladen. Kompilieren und starten Sie die Anwendung. Es sollte ein Dialog erscheinen, in dem »Hello World« steht.
Framework erweitern Wir haben Ihnen gezeigt, wie Sie ein eigenes Framework erzeugen und nutzen können. Allerdings enthält das Framework bisher nur eine einzige Klasse. Diese Klasse leitet direkt von NSObject ab. So weit, so gut. Jetzt soll das Framework um eine weitere Klasse ergänzt werden. Die neue Klasse soll allerdings von NSView und nicht von NSObject ableiten. Prinzipiell können Sie beim Erzeugen der zweiten Klasse genauso vorgehen wie bei der Erzeugung der Klasse AlertGenerator. Unterschiede ergeben sich erst bei der Benutzung der View Klasse. Außerdem möchten wir Ihnen im weiteren Verlauf dieses Kapitels noch zeigen, wie Sie für das Gradient View ein Interface Builder-Plugin entwickeln. Mit einem Plugin für dieses View können Sie im Interface Builder, wie gewohnt, das Gradient View auf ein Fenster ziehen und über den Inspektor konfigurieren. Aber eins nach dem anderen. Das eigene View soll GradientView heißen. Wie der Name schon vermuten lässt, wird das View einen Farbverlauf zeichnen. Der Farbverlauf lässt sich durch die Eigenschaften startingColor und endingColor beeinflussen. Öffnen Sie nun wieder das Projekt MyFramework. Erzeugen Sie eine neue Klasse und leiten Sie diese von NSView ab. Wie eben schon besprochen, nennen Sie diese Klasse bitte GradientView. Machen Sie die Datei GradientView.h wieder public, indem Sie das Target MyFramework auswählen. In der Tabelle auf der rechten Seite setzen Sie den Wert in der Spalte Role auf public. Die Klasse GradientView verfügt über die Eigenschaften starting797
SmartBooks
Objective-C und Cocoa – Band 2
Color (vom Typ NSColor) und endingColor (ebenfalls vom Typ NSColor). Öffnen Sie die Datei GradientView.h und passen Sie den Code wie folgt an: #import @interface GradientView : NSView { NSColor *startingColor; NSColor *endingColor; } #pragma mark Properties @property (copy) NSColor *startingColor; @property (copy) NSColor *endingColor; @end
Die Implementierung wird etwas komplizierter. Wie gewohnt wird die Methode -initWithFrame: überschrieben. Dort setzen wir Standardwerte für die Eigenschaften startingColor und endingColor. Außerdem müssen noch die Methoden -encodeWithCoder: und -initWithCoder: implementiert werden. Theoretisch ist es nicht unbedingt notwendig, diese beiden Methoden zu implementieren. Allerdings soll später auf Grundlage der Klasse GradientView ein Plugin für den Interface Builder erzeugt werden. Der Interface Builder serialisiert alle Objekte beim Speichern. Die Serialisierung und Deserialisierung funktionieren nur dann, wenn diese beiden Methoden korrekt implementiert sind. Wenn der Wert von startingColor oder endingColor geändert wird, muss sich das View neu zeichnen. Daher wird bei jedem Aufruf von -setStartingColor: und -setEndingColor: das View mittels -setNeedsDisplay: aufgefordert, sich neu zu zeichnen. Zu guter Letzt muss in -drawRect: der eigentliche Farbverlauf gezeichnet werden. Dies wird mit Hilfe der Klasse NSGradient erledigt.
GRUNDLAGEN Die Klasse NSGradient wurde mit OS X 10.5 eingeführt. Falls Sie für ältere Versionen von OS X entwickeln möchten, so bleibt Ihnen nur der Weg über das C-API CGShading. Ich schlage Ihnen die folgende Implementierung vor: #import "GradientView.h" @implementation GradientView - (id)initWithFrame:(NSRect)frame {
798
Kapitel 9
Auslieferung, Versionierung, Konfiguration
self = [super initWithFrame:frame]; if (self) { [self setStartingColor:[NSColor redColor]]; [self setEndingColor:[NSColor blueColor]]; } return self; } // Wir implementieren -encodeWithCoder:, da GradientView // später im Interface Builder zur Verfügung stehen soll. // Der Interface Builder serialisiert die Objekte und nutzt // hierzu unter anderem diese Methode. - (void)encodeWithCoder:(NSCoder *)coder { [super encodeWithCoder:coder]; [coder encodeObject:self.startingColor forKey:@"startingColor"]; [coder encodeObject:self.endingColor forKey:@"endingColor"]; } // Wir implementieren -initWithCoder:, da GradientView // später im Interface Builder zur Verfügung stehen soll. // Der Interface Builder serialisiert die Objekte und nutzt // hierzu unter anderem diese Methode. - (id)initWithCoder:(NSCoder *)coder { self = [super initWithCoder:coder]; NSString *startingKey = @"startingColor"; self.startingColor = [coder decodeObjectForKey:startingKey]; self.endingColor = [coder decodeObjectForKey:@"endingColor"]; return self; } - (void)dealloc { [self setStartingColor:nil]; [self setEndingColor:nil]; [super dealloc]; } #pragma mark Properties
799
SmartBooks
Objective-C und Cocoa – Band 2
@synthesize startingColor; - (void)setStartingColor:(NSColor *)newValue { [newValue retain]; [startingColor release]; startingColor = newValue; [self setNeedsDisplay:YES]; } @synthesize endingColor; - (void)setEndingColor:(NSColor *)newValue { [newValue retain]; [endingColor release]; endingColor = newValue; [self setNeedsDisplay:YES]; } - (void)drawRect:(NSRect)dirtyRect { [super drawRect:dirtyRect]; if([self startingColor] == nil || [self endingColor] == nil) { return; } NSArray *cols = [NSArray arrayWithObjects:self.startingColor, self.endingColor, nil]; NSGradient *grad = [[NSGradient alloc] initWithColors:cols]; [grad autorelease]; [grad drawInRect:[self bounds] angle:90.0f]; } @end
800
Kapitel 9
Auslieferung, Versionierung, Konfiguration
HILFE Sie können das Projekt in diesem Zustand als »MyFramework 02« von der Webseite des Buches herunterladen. Um Ihnen zu zeigen, dass der obige Code funktioniert, werden wir ihn auch sogleich testen.
Gradient View nutzen Jetzt soll das Gradient View getestet werden. Öffnen Sie hierzu das Projekt FrameworkConsumer. Doppelklicken Sie die Datei MainMenu.xib, um sie mit dem Interface Builder zu öffnen. Bevor Sie die Klasse GradientView nutzen können, müssen Sie die Datei GradientView.h in Xcode finden und in das Nib-Hauptfenster ziehen.
Dem Interface Builder die Klasse GradientView bekannt machen
Dies bewirkt, dass der Interface Builder nun die Klasse GradientView kennt. Würden Sie dies nicht machen, so könnte die Nib nicht korrekt geladen werden. Jetzt können Sie ein Custom View auf das Fenster ziehen und die Klasse von NSView in GradientView ändern. Speichern Sie alles ab und kompilieren Sie das Projekt. Beim Starten der Anwendung sollte im Fenster ein schöner Farbverlauf angezeigt werden.
HILFE Sie können das Projekt in diesem Zustand als »FrameworkConsumer 02« von der Webseite des Buches herunterladen. 801
SmartBooks
Objective-C und Cocoa – Band 2
Interface Builder-Plugin für das Gradient View Der Umgang mit dem Gradient View, welches in den vorherigen Abschnitten entwickelt wurde, kann recht anstrengend werden. Sie müssen im Interface Builder ein Custom View erzeugen und dessen Klassenname in GradientView ändern. Wenn Sie dann den angezeigten Farbverlauf abändern möchten (weil Ihnen die Standardfarben nicht gefallen), so müssen Sie zunächst ein Outlet auf das Gradient View erstellen, die Verbindung zwischen einem Ihrer Controller und dem Gradient View herstellen, um dann im Code die Farben des Farbverlaufes anzupassen. Dies ist eine sehr ermüdende und fehleranfällige Tätigkeit. Abhilfe schafft da ein Interface Builder-Plugin. Ein Interface Builder-Plugin führt dazu, dass das Gradient View in der Library des Interface Builders auftaucht. Mittels Drag & Drop kann das Gradient View auf ein Fenster gezogen und mit dem Inspektor konfiguriert werden. Ein Interface Builder-Plugin macht einmalig Arbeit, sorgt dann allerdings für eine Beschleunigung bei der Entwicklung. Jetzt wird für das Gradient View ein Plugin entwickelt. Den Code für das Gradient View werden wir mit Copy & Paste vom Projekt MyFramework übernehmen. So kommen wir schneller zum Ziel. In der Praxis ist es natürlich nicht üblich, dass der Code mittels Copy & Paste übernommen wird. Darauf gehen wir am Ende noch ein und zeigen Ihnen, wie man es mit ein wenig mehr Aufwand hätte besser machen können. Jetzt aber an die Arbeit.
HILFE Sollten Sie die vorherigen Abschnitte übersprungen haben, so können Sie sich das benötigte Gradient View als »MyFramework 2« von der Webseite des Buches herunterladen.
Projekt anlegen Der Xcode-Gott ist gnädig mit uns, denn es gibt ein Template für Interface BuilderPlugins. Legen Sie ein neues Projekt an. Als Projektart wählen Sie Interface Builder Plug-in. Nennen Sie das Projekt unbedingt Gradient.
AUFGEPASST Es ist sehr wichtig, dass Sie das Projekt Gradient nennen. Man könnte auch einen anderen Namen wählen. Allerdings müssten Sie dann entweder die Klasse GradientView entsprechend umbenennen oder die vom Template erzeugten Dateien anpassen. Da wir Ihnen schnell ein Erfolgserlebnis beschaffen wollen, sollten Sie Ihren Drang zum Experimentieren ein wenig aufschieben.
802
Kapitel 9
Auslieferung, Versionierung, Konfiguration
Projektstruktur Das Projekt besteht aus den Targets Gradient und GradientFramework. Gradient erzeugt das eigentliche Plugin, welches vom Interface Builder geladen und verwendet wird. Dieses Target enthält Code und Resourcen, die für den Interface Builder bestimmt sind. Das Target GradientFramework enthält den eigentlichen Code des Views, für welches das Plugin entwickelt wird. In unserem Falle enthält das GradientFramework lediglich die Klasse GradientView. Wir möchten uns aber das Target Gradient noch ein wenig genauer anschauen. Die Datei GradientView.classdescription enthält die Beschreibung der Klasse GradientView. Die wichtigsten Inhalte der Klassenbeschreibung sind der Name der Superklasse sowie der Name der Klasse selbst und die verfügbaren Outlets und Actions. Für unsere Zwecke ist der Inhalt der Klassenbeschreibung ausreichend. Neben der Klassenbeschreibung sind noch die Klassen Gradient und GradientInspector zu finden. Die Klasse Gradient ist der Einstiegspunkt für den Interface Builder. Der Klasse GradientInspector entnimmt der Interface Builder den Namen der Nib-Datei, welche den Inspektor enthält. Über den Inspektor lassen sich später die Eigenschaften startingColor und endingColor des Gradient Views einstellen. Mit Hilfe der Datei GradientViewIntegration.m wird die Klasse GradientView um die Methoden -ibPopulateKeyPaths: und -ibPopulateAttributeInspectorClasses: erweitert. Für den Anfang ist auch der Inhalt dieser Datei für unsere Zwecke ausreichend. Testen des Plugins Wenn Sie jetzt mit Xcode das Target All kompilieren und starten, so öffnet sich der Interface Builder und lädt automatisch das Gradient-Plugin. In der Library des Interface Builders wird auch schon ein Platzhalter für das Gradient View angezeigt.
HILFE Sie können das Projekt in diesem Zustand als »Gradient 1« von der Webseite des Buches herunterladen.
Gradient View übernehmen Öffnen Sie die Datei GradientView.h aus Ihrem Framework und kopieren Sie den Inhalt in die Zwischenablage. Ersetzen Sie den Inhalt der Datei GradientView.h des Projektes namens Gradient mit dem Inhalt der Zwischenablage. Analog führen Sie diese Schritte auch für die Datei GradientView.m durch.
HILFE Sie können das Projekt in diesem Zustand als »Gradient 2« von der Webseite des Buches herunterladen.
803
SmartBooks
Objective-C und Cocoa – Band 2
Platzhalter anpassen Mit der Datei GradientLibrary.nib kann man das View festlegen, welches in der Library als Platzhalter angezeigt wird. Öffnen Sie diese Datei mit dem Interface Builder. Lassen Sie sich das View namens Library Objects anzeigen. Markieren Sie das Textfeld mit der Beschriftung Example und alle Objekte, die sich darunter befinden. Drücken Sie anschließend die Taste zum Entfernen aller markierten Objekte. Der verbleibende weiße Kasten enthält ein Custom View (hellblaue Box). Markieren Sie das Custom View und ändern Sie dessen Klasse in GradientView ab. Speichern Sie Ihre Änderungen und kompilieren Sie wieder das Target All. Wenn Sie nun das Target wieder starten, so lädt der Interface Builder wieder das Plugin. Diesmal wird kein leerer Platzhalter angezeigt, sondern das Gradient View. Sie können das Gradient View jetzt aus der Library auf ein Fenster ziehen und wie gewohnt konfigurieren. Die Eigenschaften startingColor und endingColor lassen sich allerdings noch nicht einstellen.
HILFE Sie können das Projekt in diesem Zustand als »Gradient 3« von der Webseite des Buches herunterladen.
Inspektor implementieren Markiert man im Interface Builder ein Gradient View, so lässt sich über den Inspektor schon so einiges einstellen. Was sich nicht einstellen lässt, sind die speziellen Eigenschaften (startingColor und endingColor) des Gradient Views. Dies wollen wir nun ändern. Öffnen Sie hierzu die Datei GradientViewIntegration.m und passen Sie den Code wie folgt an. #import #import #import "GradientInspector.h" @implementation GradientView ( GradientView ) - (void)ibPopulateKeyPaths:(NSMutableDictionary *)keyPaths { [super ibPopulateKeyPaths:keyPaths]; NSArray *keys = [NSArray arrayWithObjects:@"startingColor", @"endingColor", nil]; id attrbs = [keyPaths objectForKey:IBAttributeKeyPaths]; [attrbs addObjectsFromArray:keys]; }
804
Kapitel 9
Auslieferung, Versionierung, Konfiguration
- (void)ibPopulateAttributeInspectorClasses:(NSMutableArray *)cls { [super ibPopulateAttributeInspectorClasses:cls]; [cls addObject:[GradientInspector class]]; } @end
In der Methode -ibPopulateKeyPaths: fügen Sie beim übergebenen Dictionary an passender Stelle die Schlüsselpfade startingColor und endingColor ein. Mit diesen Informationen kann der Interface Builder für diese Eigenschaften unter anderem Undo realisieren. Jetzt muss noch das User-Interface angepasst werden. Öffnen Sie hierzu die Datei GradientInspector.xib und öffnen Sie das View namens Inspector View. Löschen Sie dort alle Objekte bis auf das Textfeld in der linken oberen Ecke. Ändern Sie den Text des Textfeldes von Label auf Starting Color. Platzieren Sie rechts vom Textfeld ein Color-Well. Duplizieren Sie das Textfeld und das Color-Well und platzieren Sie die Kopie unter dem Original. Geben Sie dem unteren Textfeld den Titel Ending Color. Das User-Interface ist fertig. Das obere und untere Color-Well müssen allerdings noch gebunden werden. Binden Sie das obere ColorWell wie folgt: Bind to: File’s Owner Controller Key: Model Key Path: inspectedObjectsController.selection. startingColor
Analog binden Sie das untere Color-Well: Bind to: File’s Owner Controller Key: Model Key Path: inspectedObjectsController.selection.endingColor
Fertig. Wieder kompilieren und starten. Jetzt sollten Sie im Inspektor des Interface Builders den Farbverlauf konfigurieren können.
HILFE Sie können das Projekt in diesem Zustand als »Gradient 4« von der Webseite des Buches herunterladen.
Plugin ausliefern Ein Starten des Targets All hat zur Folge, dass das Plugin automatisch in den Interface Builder geladen wird. Wenn Sie den Interface Builder beenden und neu starten, 805
SmartBooks
Objective-C und Cocoa – Band 2
so wird das Plugin nicht wieder automatisch geladen. Wir zeigen Ihnen nun, wie Sie das Plugin richtig deployen. Fügen Sie dem Target Gradient eine neue CopyBuild-Phase hinzu. Als Destination wählen Sie Frameworks. Ziehen Sie die Datei Gradient.framework in diese Build-Phase. Anschließend rufen Sie die Informationen des Targets GradientFramework auf. Ändern Sie in den Build-Eigenschaften den Wert der Eigenschaft Installation Directory auf »@loader_path/../Frameworks«. Jetzt können Sie mit Xcode einen release-Build erzeugen. Der relelase-Build erzeugt unter anderem die Datei Gradient.ibplugin. Ein Doppelklick auf diese Datei installiert das Plugin dauerhaft im Interface Builder.
HILFE Sie können das Projekt in diesem Zustand als »Gradient 5« von der Webseite des Buches herunterladen.
Plugins in der Praxis In den vorangegangenen Abschnitten wurde ein Plugin für das Gradient View entwickelt. Hierbei wurde die Implementierung des Gradient Views mit Copy & Paste von einem anderen Projekt übernommen. Dies würde man in der Praxis natürlich anders machen, da dieses Vorgehen einige Nachteile hat. Der wichtigste Nachteil ist wohl der, dass der Code des Gradient Views in mehreren Projekten landet. Dies ist unübersichtlich und ineffizient. Wenn jemand einen Fehler im Gradient View behebt, so müsste man die Änderungen von Hand übernehmen. Daher geht man in der Praxis einen leicht anderen Weg. Man hat im Wesentlichen zwei Möglichkeiten. Das Plugin im Framework Das Projekt MyFramework selbst könnte das Interface Builder-Plugin schon mitbringen. Man spart sich dann das Anlegen eines zusätzlichen Projektes für das Plugin. Glücklicherweise bietet Xcode unterstützend Vorlagen auf Dateibasis an, die das Entwickeln eines Plugins für den Interface Builder vereinfachen. Es gibt Vorlagen für Klassenbeschreibungen, Inspektoren und Libraries. Diese legt man nacheinander an und baut sich so das Plugin zusammen. Das Plugin als Abhängigkeit vom Framework Die zweite Möglichkeit sieht vor, dass das Plugin als eigenes Projekt realisiert wird. Statt aber den Code für das GradientView mit Copy & Paste zu übernehmen, definiert man einfach das entsprechende Framework (MyFramework) als Abhängigkeit hinzu.
806
Kapitel 9
Auslieferung, Versionierung, Konfiguration
Sourcekontrollsysteme Eine Software wird nicht fertig. Vielmehr wird sie in der Regel ständig weiter entwickelt – bis sie dann eingestellt wird. Das führt dazu, dass man entsprechend verschiedene Versionen des Sourcecodes auf der Festplatte liegen hat. Gut, man kann die Versionen abspeichern, indem man das Projektverzeichnis kopiert. Das ist aber schon nicht sonderlich elegant, weil auch in den verschiedenen Versionen gleich gebliebene Sourcetexte doppelt abgespeichert werden. Man bedient sich vielmehr sogenannter Versionskontrollsysteme oder Versionierungssysteme. Wir mögen allerdings diesen Begriff nicht besonders gerne, da nämlich üblicherweise Versionskontrollsysteme auch ein zweites Problem lösen: das Arbeiten in einer Gruppe. Stellen Sie sich vor, dass Sie mit jemand anderem eine Software entwickeln. Öffnen Sie die Implementierung einer Klasse und führen Änderungen durch, so darf das Ihr Kumpel nicht gleichzeitig machen. Andernfalls würden die Änderungen von demjenigen, er zuletzt speichert, die Änderungen des anderen überschreiben. Klingt nicht gut! Also ist Sourcekontrollsystem (Source-Control, Source-Control-Management, SCM) schon irgendwie passender. Aber gut, »Versionskontrollsystem« hat sich als Bezeichnung durchgesetzt.
Konzept Xcode unterstützt die Arbeit mit verschiedenen Sourcekontrollsystemen. Die prinzipielle Arbeitsweise ist bei allen SCM jedoch gleich: Man speichert seine Sourcen an einem zentralen Ort, dem Repository (Archiv). Wenn man etwas ändern will, holt man sich aus diesem Archiv eine Arbeitskopie. An dieser führt man seine Änderungen durch. Ist man fertig, so wird die (geänderte) Datei wieder im Archiv abgelegt. Damit das aber anders als mit reinem Dateikopieren nicht vollends durcheinander gerät, werden freilich keine unmittelbaren Dateioperationen durchgeführt. Vielmehr verhält es sich so, dass ein SCM-Client Befehle erhält. Hat man das Archiv als Server über ein Netzwerk eingerichtet, schickt der Client die Befehle dann an den Server, der die entsprechenden Operationen ausführt. Liegt lediglich ein lokales dateibasiertes Archiv vor, so ändert der Client selbst das Archiv. Aber immer führt der Weg über ein Tool, eben den SCM-Client.
807
SmartBooks
Objective-C und Cocoa – Band 2
Archiv
Server
Client Import
Checkout
Der Programmierer verwaltet das Projekt grundsätzlich über einen Client.
Arbeitskopie
Natürlich existieren zahlreiche Sourcekontrollsysteme. Und jedes dieser Systeme funktioniert anders. Ist allerdings erst einmal ein Archiv eingerichtet, so bietet uns – glücklicherweise – Xcode ein Interface für die verschiedenen Clients.
TIPP Wie glücklich das ist, hängt wohl vom Betrachter ab. Manche – vor allem alte – Recken schwören daher darauf, das Versionskontrollsystem vom Terminal aus oder mittels eines GUI-Frontends selbst zu bedienen. Hier geht es um Xcode. Für die Terminalbedienung schauen Sie bitte in die Dokumentation der verschiedenen Versionskontrollsysteme. Es ist also für die tägliche Arbeit nicht erforderlich, den Client selbst anzusprechen. Das Einrichten des Archivs werden wir allerdings gleich einmal im Terminal erledigen. Von Hause aus versteht sich Xcode mit:
•
CVS – Concurrent Versions System: einem vor allem im Open-Source-Bereich beliebtes und kostenfreies Versionierungssystem
•
SVN – Subversion: ebenfalls kostenfrei. Dieses System werden wir gleich exemplarisch benutzen. Es dürfte als moderner als CVS gelten.
•
Perforce: ein kostenpflichtiges Sourcekontrollsystem 808
Kapitel 9
Auslieferung, Versionierung, Konfiguration
Es ist müßig, über die Vor- und Nachteile der verschiedenen Sourcekontrollsysteme zu diskutieren. Es ist nicht nur müßig, sondern bereits 2384433476 Mal gemacht worden. Wenn wir das aber – ohne eine repräsentative Umfrage gemacht zu haben, die wissenschaftlichen Ansprüchen genügt – richtig sehen, nutzen die meisten Mac-Entwickler Subversion.
Anlegen von Archiv und Projekt Bevor wir uns um das alltägliche Arbeiten mit einem Versionskontrollsystem kümmern, ist es zunächst erforderlich, ein Archiv – im Jargon der Versionskontrollsysteme »Repository« genannt – mit einem Projekt anzulegen. Grundsätzlich ist es möglich, mehrere Projekte oder gar alle Projekte in einem Archiv zu halten. Sie sollten jedoch für unabhängige Projekte gesonderte erzeugen. Hängen allerdings Projekte zusammen, so ist es eine gute Idee, sich diese ein Archiv teilen zu lassen. Ein weiterer Grund, mehrere Repositorys zu haben, ist die Art des Zugriffs. Für Dinge, die Sie zuhause alleine bei Lust und Laune programmieren, empfiehlt es sich, den Zugriff auf das Archiv anders zu gestalten als bei einem großen Projekt, welches Sie mit Leuten auf der ganzen Welt gemeinsam programmieren.
Anlegen des Repositorys Unsere erste Aufgabe ist es, ein Repository anzulegen. Hierzu öffnen Sie bitte zunächst einen Terminal. Wie bereits angekündigt, werden wir mit SVN arbeiten. Da dieses bereits mitgeliefert wird, können wir uns Installationsorgien sparen. Wir werden auch im Dateimodus arbeiten, also ohne Server, den wir erst aufsetzen müssten. Daher mit vollem Mute: $ svnadmin create /Users/Shared/SourceControlExample
svnadmin ist das Tool zum Verwalten von Archiven. Über den Befehl create sagen wir ihm, dass es ein Repository unter dem angegebenen Pfad anlegen soll. Der Pfad selbst ist vielleicht bemerkenswert: Wie Sie bereits gelernt haben, ist /Users/ Shared das Wurzelverzeichnis für Dateien, die allen Usern zur Verfügung stehen sollen. In diesem Beispiel geht es auch um das Arbeiten in einer Gruppe, weshalb wir bewusst ein freies Verzeichnis gewählt haben, auf das Sie von verschiedenen Benutzerkonten aus zugreifen können. (Der Zugriff erfolgt in der Realität freilich von verschiedenen Rechnern auf ein gemeinsames Verzeichnis, welches in einem Netzwerk freigegeben ist. Wir wollen Sie aber nicht extra für dieses Kapitel zur Anschaffung eines zweiten Macs zwingen.)
809
SmartBooks
Objective-C und Cocoa – Band 2
Der letzte Teil des Pfades ist dann unser Repository. Das Verzeichnis wird automatisch von svnadmin angelegt. Bitte prüfen Sie das nach, indem Sie im Finder ausgehend von Ihrer Systemplatte (Standard: Macintosh HD) den Ordner Benutzer und darin Für alle Benutzer suchen.
Unser Archiv (Repository) im Finder
Wir haben also ein Archiv als Verzeichnis erstellt.
POWER Man kann bei SVN auch eine Datenbank nutzen. Hierzu muss die Option --fs-type bdb (Typ: Berkeley Database) hinzugefügt werden. Gerade zum Lernen hat die Dateiversion des Archivs jedoch den Vorteil, dass wir unmittelbar das Geschehen im Finder beobachten können.
Repository in Xcode anmelden Damit Xcode später mit dem Archiv arbeiten kann, müssen wir es in Xcode bekannt machen. Xcode kennt ein Archiv, wenn es eine sogenannte Repository-Configuration dafür hat. Um diese anzulegen, öffnen Sie bitte die Einstellungen (Xcode | Preferences) und suchen nach dem Tab SCM. Links finden Sie die (noch leere) Liste aller Archive. Klicken Sie unten auf den Button +. Im Sheet geben Sie als Namen Source-Control Example an. Xcode legt uns die Konfiguration an. Unterhalb der Eingabefelder können Sie jedoch den Hinweis Incomplete Configuration erkennen. Wir müssen also noch zusätzliche Angaben vornehmen. Das Schöne: Ständig überprüft Xcode, ob unsere Angaben richtig sind, und aktualisiert dabei unten den Status.
810
Kapitel 9
Auslieferung, Versionierung, Konfiguration
Die Repository-Configuration für unser Archiv
Fangen wir an: Den Namen lassen wir unverändert. Auch das Feld URL darunter nutzen wir nicht für die Eingabe. Xcode wird es automatisch aus den anderen Eingaben erzeugen. Also beginnen wir mit Scheme, der Zugriffsart. Wir haben ein dateibasiertes Archiv eingerichtet, auf welches wir ohne Client-Server-Funktionalität zugreifen wollen. Daher tragen wir hier file ein. Aus diesem Grunde benötigen wir ansonsten auch nur noch das Feld Path, welches den Pfad zum Repository angibt. Tragen Sie hier den Pfad des soeben angelegten Verzeichnisses ein: /Users/Shared/SourceControlExample. Unterhalb der Eingabefelder sollte jetzt Authenticated erscheinen. Klicken Sie auf OK.
Projekt erzeugen und mit dem Repository verbinden Damit wir etwas zum Experimentieren haben, ist es freilich erforderlich, ein Projekt anzulegen. Dies erfolgt lokal, also außerhalb des Repositorys. Wir verschieben es dann in dasselbe. Erzeugen Se also bitte zunächst ein Projekt mit dem Namen SourceControl aus dem Template Cocoa Application ohne Unterstützung von Dokumenten und Core Data. Wählen Sie nun in der Projektleiste Groups & Files den obersten Eintrag an und öffnen Sie das Info-Fenster. Oben rechts bei Configure Roots & SCM im Tab General können Sie angeben, in welchem Repository das Projekt archiviert werden 811
SmartBooks
Objective-C und Cocoa – Band 2
soll. Wählen Sie in der rechten Spalte das vorhin bekannt gemachte Repository Source-Control Example. Schließen Sie das Fenster wieder und schauen Sie in die Projektleiste Groups & Files: Aktivieren Sie mit einem Klick auf die Titelzeile der Projektleiste die neue Spalte SCM. Wie wir später sehen werden, zeigt diese Spalte den Status unserer Dateien im Verhältnis zum Archiv an.
Die neue Spalte zeigt den SCM-Status an.
Allerdings weiß jetzt Xcode nur, dass dieses Projekt zu dem entsprechenden Repository gehört. Es befindet sich jedoch noch nicht in dem Archiv. Um dies zu bewerkstelligen, müssen wir es explizit in das Archiv importieren. Um dies zu bewerkstelligen, schließen Sie das Projekt zunächst. Im Menü SCM findet sich der Eintrag Repositories. Klicken Sie hierauf. Es erscheint ein neues Fenster, welches gleichermaßen Ihre Repository-Verwaltung ist. Wählen Sie in der Werkzeugleiste das Item Import. Im darauf folgenden Dateidialog suchen Sie bitte den Projektordner (nicht die Projektdatei mit der Endung xcodeproj!) und geben Sie unten bitte einen Kommentar ein. Klicken Sie dann auf Import. Nach kurzer Zeit erscheint ein weiteres Sheet, welches den Erfolg anzeigt. Bestätigen Sie das Sheet.
Der Repository-Browser dient zur Verwaltung.
812
Kapitel 9
Auslieferung, Versionierung, Konfiguration
In dem Fenster finden Sie drei Bereiche:
•
Links befindet sich die Liste aller Repositorys. Wenn Sie nicht schon heimlich weitere angelegt haben, dann sehen Sie lediglich das von uns soeben angelegte. Mit der Schaltfläche Configure in der Toolbar gelangen Sie übrigens zu den Einstellungen.
•
Oben sehen Sie einen einfachen Dateibrowser. Sie können damit ähnlich wie im Finder durch die Verzeichnisse klicken und sich den Inhalt anzeigen lassen.
•
Unten befindet sich ein Log. Hier wird die Kommunikation zwischen Xcode und dem jeweiligen SCM-Client aufgelistet.
Löschen Sie jetzt bitte Ihr lokales Projektverzeichnis auf der Festplatte und klicken Sie auf Checkout. Hiermit legen wir jetzt die erste Arbeitskopie des Projektes an. Wenn Sie einen Speicherort angewählt und auf Checkout geklickt haben, fragt Sie Xcode, ob Sie gleich das Projekt öffnen wollen. Ja, das wollen wir, da wir ja an dem Projekt arbeiten. Es erscheint unser gewohntes Projektfenster. Schließen Sie das Repository-Fenster wieder. Wir haben jetzt den Zustand erreicht, dass sich ein Projekt im Repository befindet, welches wir in der lokalen Kopie ändern können.
Lokal arbeiten Während der Arbeit an einem Projekt können verschiedene Situationen eintreten:
•
Sie ändern eine Datei. Damit sind Ihr Sourcecode und der des Archives asynchron. Um dies wieder zu ändern, müssen Sie also Ihren Sourcecode wieder dem Projekt hinzufügen. Dies nennt man im IT-Denglisch »committen«.
•
Jemand anderes ändert eine Datei und schreibt sie zurück ins Archiv. Auch jetzt stimmen Ihre Dateien nicht mehr mit denen des Archivs überein. Sie müssen die Dateien neu laden: updaten.
•
Sie und jemand anderes ändern dieselbe Datei. Jetzt ist unklar, welche Version gelten soll. Man nennt dies eine »Kollision«.
•
Grundsätzlich können sich aber nicht nur einzelne Dateien ändern, sondern auch die Projektstruktur, wenn jemand Dateien hinzufügt oder löscht oder sonstwie die Struktur verändert. Auch hier muss entsprechend ins Archiv oder aus dem Ordner synchronisiert werden.
Da Sie nur ein Mensch sind, müssen wir Sie kurz in die Schizophrenie führen. Wir werden die verschiedenen Beabreitungszustände testen, indem Sie sich zweimal 813
SmartBooks
Objective-C und Cocoa – Band 2
einloggen. Bitte legen Sie sich also über Apfelmenü | Systemeinstellungen | Benutzer zunächst einen weiteren Benutzeraccount zu. Wir haben dafür den Namen SCM Tester und den Kurznamen scmtester verwendet. Bitte schalten Sie auch den schnellen Benutzerwechsel ein (Apfelmenü | Systemeinstellungen … | Benutzer | Anmeldeoptionen | Menü für schnellen Benutzerwechsel zeigen als:), damit Sie sich nicht jedes Mal aus- und einloggen müssen. Nachdem dies erledigt ist, lassen Sie sich von dem Archiv-Verzeichnis /Users/ Shared/SourceControlExample das Infofenster anzeigen. Sie müssen dem neuen Benutzer die Rechte geben, in dieses Verzeichnis zu schreiben. Zunächst öffnen Sie bitte das Schloss unten rechts, wobei Sie sich als Administrator anmelden müssen. Dann klicken Sie in der Rechteverwaltung auf den Plus-Button und fügen den neuen Benutzer hinzu. Ist dies erledigt, so erlauben Sie dem neuen Benutzer das Lesen und Schreiben auf das Verzeichnis. Schließlich wenden Sie die neuen Einstellungen auf den gesamten Ordner an.
Ihr zweites Ich bekommt ebenfalls Zugriffsrechte auf das Repository.
GRUNDLAGEN Dies ist kein Buch über Administration. Wenn Sie also tatsächlich in einer Arbeitsgruppe in einem Netzwerk arbeiten, dann sollten Sie sich schon Gedanken über die Gestaltung der Zugriffsrechte machen. Wechseln Sie gleich zu dem neuen Benutzer, freilich ohne Ihren aktuellen Account zu schließen (Schneller Benutzerwechsel). Dort starten Sie Xcode und erzeugen sich 814
Kapitel 9
Auslieferung, Versionierung, Konfiguration
eine Repository-Config, wie Sie es auch gerade hier gemacht hatten. Öffnen Sie über SCM | Repositories die Liste, wählen Sie das Projekt und checken Sie es aus. Nachdem dies erfolgt ist, sollten Sie gleich das Infofenster für SourceControl in der Projektleiste öffnen und das Projekt auch auf diesem Account mit dem Repository verbinden. Wechseln Sie wieder zum ersten Benutzer.
Eigene Änderungen vornehmen und hochladen Zunächst wollen wir eine Datei selbst bearbeiten. Öffnen Sie in der Gruppe SourceControl | Other Sources die Datei main.m. Fügen Sie vor main() einen kleinen Kommentar ein: … // Hier wurde etwas lokal geaendert. int main(int argc, char *argv[]) …
Speichern Sie das Ganze. Sie können erkennen, dass in der Projektleiste neben unserer Datei ein M auftaucht. Dies steht für modified, also verändert. Damit will uns Xcode sagen, dass sich unsere lokale Version von derjenigen unterscheidet, die sich im Repository befindet. Nein, wir schreiben unsere Änderung jetzt noch nicht in das Repository zurück. Denn dort landet nur etwas, was Sie getestet haben! Die Version, die sich im Archiv befindet, sollte immer kompilierbar sein! Also zunächst Build & Go. Mangels Funktionalität unseres Programms erübrigt sich jetzt freilich ein Test. Beenden Sie das Programm gleich wieder. Jetzt kommt der große Moment der Bestätigung. Wählen Sie die Datei in der Projektleiste Groups & Files aus und klicken Sie dann auf SCM | Commit Changes. Damit wird jetzt die geänderte Fassung ins Repository übertragen. Sie sollten stets, auch dann, wenn Sie im Stress sind, eine Bemerkung dazu schreiben, was Sie geändert haben. Später ist das Gold wert!
TIPP Sie können ebenfalls mit SCM | Commit Entire Project… das gesamte Projekt ins Repository schicken. Bedenken Sie aber, dass in diesem Falle auch diejenigen Dateien wieder ins Repository gelangen, die nicht von Ihnen, aber von jemand anderem verändert wurden. Sie werden gleich eine Möglichkeit kennen lernen, dies vor dem Commit nachzuprüfen. Dies spielt freilich keine Rolle, wenn Sie SCM nur für sich selbst zur Versionskontrolle einsetzen – und außerhalb dieses Buches nicht schizophren sind.
815
SmartBooks
Objective-C und Cocoa – Band 2
Fremde Änderungen herunterladen Wir haben jetzt also über Ihr reguläres Benutzerkonto Änderungen vorgenommen. Was geschieht mit denen beim anderen Benutzer, der ja vorher das Projekt geladen hatte? Um dies zu testen, wechseln Sie bitte auf den neuen Benutzer.
Wer hat an der Uhr gedreht?
Wenn Sie in der Menüleiste SCM | Refresh Entire Project… anklicken, werden Sie bemerken, dass in der Projektleiste neben der Datei ein U erscheint. Dieses steht für updated und bedeutet, dass ein anderer (aus der Sicht des neuen Benutzers) eine Änderung vorgenommen hat. Um diese Änderungen zu erhalten, müssen Sie in der Menüleiste SCM | Update (bzw. Update Entire Project) anwählen. Machen Sie das bitte und schauen Sie sich die Datei an.
Kollision Können mehrere Programmierer Änderungen vornehmen, so liegt es auf der Hand, dass Konflikte auftreten können. Auch dies wollen wir einmal durchspielen. Da Sie sich noch beim neuen Benutzer befinden, bitte ich Sie darum, den Kommentar in main.m zu ändern, und zwar in // SCM Tester geaendert
Geben Sie die Änderung gleich an das Repository weiter, indem Sie einen Commit ausführen. Wechseln Sie wieder zu Ihrem normalen Nutzer. Dieser Benutzer hat noch die Version vor der letzten Änderung (und dem Commit) von main.m. Er weiß erst einmal nichts von der Änderung, da er ja nicht pau816
Kapitel 9
Auslieferung, Versionierung, Konfiguration
senlos Refreshes machen kann. Deshalb ändert er naiv ebenfalls den Kommentar in main.m: // Hauptbenutzer geaendert
Sobald Sie die Änderung speichern, bemerken Sie, dass in der Projektleiste ein U auftaucht: Xcode sagt uns also, dass hier ein Update aus dem Repository erforderlich ist. Das ist aber jetzt natürlich nicht der Sinn der Sache: Schließlich wollen wir unsere Änderungen auch nicht verlieren. Tun wir auch nicht. Führen Sie bitte das Update durch. Sie erkennen jetzt, dass neben den Dateien ein C erscheint, welches für Conflict steht. Xcode bemerkt also, dass sich unsere Änderungen mit denen des anderen Benutzers beißen. Sie können mit einem Doppelklick die Datei öffnen, um sich die Unterschiede anzuschauen: … #import > .r16 int main(int argc, char *argv[]) …
Dies bedeutet im Wesentlichen: Sie (.mine) haben den ersten Text eingegeben, in dem Repository findet sich in der Version 16 (Revision 16, .r16) indessen ein anderer Text. (Selbstverständlich kann bei Ihnen eine andere Versionsnummer auftauchen.) Eine andere Möglichkeit, Unterschiede zu finden, findet sich in der Menüleiste: SCM | Compare With. Wenn Sie hier etwa die letzte Version (Latest) wählen, so erscheint ein File-Compare mit einer Gegenüberstellung. Sieht halt schöner aus. Wichtig ist hier die Vergleichsmöglichkeit mit älteren (etwa gut getesteten und fehlerfreien) Versionen. Ähnlich funktioniert SCM | Diff With, welches graphisch nicht aufbereitet ist, dafür einen Überblick bietet. Ändern Sie jetzt den Text so, dass beide Kommentare enthalten sind. Man ist ja friedlich … Sie müssen jetzt aber noch über SCM | Resolved Xcode mitteilen, dass Sie sich darum gekümmert haben. Committen Sie diese Version.
817
SmartBooks
Objective-C und Cocoa – Band 2
Änderungen der Projektstruktur Bisher haben Sie nur bestehende Dateien verändert. Wie Sie aber wissen, können auch Dateien hinzugefügt oder gelöscht werden, etwa wenn eine neue Klasse (mit entsprechender Datei) erzeugt wird. Machen wir das doch einfach mal, und Sie legen sich eine neue Klasse MyClass an (File | New File…), wobei Sie als Vorlage Objective-C Class und NSObject verwenden. Sie bemerken jetzt zwei Arten von Mitteilungen in der Projektleiste:
•
Vor der obersten Gruppe von Group & Files erscheint ein M. Dies bedeutet, dass sich unsere Projektdatei verändert hat. Richtig ist das schon, denn in der Projektdatei werden die zum Projekt gehörenden Dateien gespeichert, also auch MyClass.m und MyClass.h. Kommt eine hinzu, ändert sich damit die Projektdatei.
•
Vor den beiden Dateien der Klasse erscheinen Fragezeichen. Dies bedeutet, dass Xcode keine entsprechenden Dateien im Repository finden kann. Auch dies ist richtig, da wir sie ja noch nicht dorthin geschrieben haben.
TIPP Manchmal vergisst Xcode die Markierungen wieder. In diesem Falle wählen Sie bitte SCM | Refresh Entire Project in der Menüleiste an. Zunächst schreiben wir die Dateien ins Archiv. Wählen Sie diese dazu in der Projektleiste aus und klicken Sie dann auf SCM | Add to Repository. Jetzt erschein ein A (added, hinzugefügt) neben den Dateien. Mit einem SCM | Commit Changes verschwindet auch dieses. Wenn Sie beim neuen Benutzer nachschauen, werden Sie die Dateien jedoch nicht finden, auch nicht nach einem Update auf das gesamte Projekt. Dies liegt eben daran, dass die Projektdatei in dem Repository noch nicht die neuen Dateien kennt. Wir haben ja bisher lediglich die neuen Dateien hinzugefügt, die geänderte Projektdatei jedoch noch nicht aktualisiert. Daher müssen wir diese beim Standardbenutzer auch noch comitten. Da steht ja auch noch ein M. Wählen Sie diese an und dann in der Menüleiste SCM | Commit Entire Project… Nun wechseln Sie erneut zum neuen Benutzer und führen Sie zunächst ein Refresh auf das gesamte Projekt durch (SCM | Refresh Entire Project). Zunächst erscheint wieder ein U, um uns die Änderung mitzuteilen. Also übernehmen wir diese mit SCM | Update Entire Project. Da jetzt die Projektdatei selbst neu geladen wird, also Xcode sozusagen die Grundlage entzogen wird, erscheint zunächst eine Rückfrage. Diese bestätigen Sie bitte mit einem Klick auf Read from Disk. Danach schließt sich kurz das Projektfenster, und das Projekt wird mit unserer neuen Klassen geladen. Ist doch alles viel einfacher, als von Hand im Dateisystem herumzufuchteln … 818
Kapitel 9
Auslieferung, Versionierung, Konfiguration
Lokalisierungen Ein echtes Problem stellt es dar, wenn Sie eine Lokalisierung hinzufügen. Dies liegt daran, dass dies in der Verzeichnisstruktur zu einem neuen Ordner führt. Xcode legt aber keine Ordner an. Der Ordner ist auch nicht in der Projektleiste Groups & Files mit seinem Inhalt – also etwa alle deutschsprachigen Dateien – vorhanden. Vielmehr kehrt die Projektleiste die Ansicht um. Ein einfaches Hinzufügen der Lokalisierung funktioniert jedenfalls nicht. Um damit irgendwie umgehen zu können, hat sich für uns nur ein System durchgesetzt:
• • •
Fügen Sie die lokalisierte Fassung einer Nib-Datei, etwa MainMenu.xib hinzu. Öffnen Sie die Anzeige der Repositorys mit SCM | Repositories. Ziehen Sie die neue Fassung – etwa deutsch – aus der Projektleiste auf den Eintrag in der Repository-Liste, hier also SourceControl. Es wird dann das Sprachverzeichnis mit allen entsprechenden Dateien – zu diesem Zeitpunkt allerdings nur eine – ins Repository kopiert.
•
Schließen Sie das Projekt. Schließen Sie Xcode. Löschen Sie Ihr lokales Verzeichnis komplett.
•
Starten Sie wieder Xcode und öffnen Sie erneut das Repository-Fenster (SCM | Repositories). Führen Sie jetzt einen neuen Checkout durch. Dies muss für alle lokalen Kopien gemacht werden, also von allen Benutzern.
Insgesamt ist die Lokalisierung im Sourcekontrollsystem eine liebe Mühe, die im Zweifel eigene Arbeiten im Repository-Fenster bedingt mit nachfolgendem neuen Checkout. Der allgemeine Rat ist, sich mit den SCM-Clients im Terminal zu beschäftigen, wenn es an Lokalisierungen geht. Die Befehle zur Erzeugung der Verzeichnisse entnehmen Sie bitte der Dokumentation zu dem von Ihnen verwendeten Client.
Rollback Eine wichtige Funktion von Sourcekontrollsystemen ist nicht nur die gemeinsame Weiterentwicklung, sondern auch der Rücksprung zu einer bestehenden Version. Manchmal vergaloppiert man sich derartig in der Softwareentwicklung, dass man zu sich selbst ruft: »Hätte ich doch noch …«. Dies dürfte übrigens eine der Stärken von Versionskontrollsystemen sein – oder von Time Machine … Wie bereits erwähnt, können Sie mit SCM | Compare With | Revision… und SCM | Diff With | Revision… die aktuelle Fassung einer Datei mit einer älteren vergleichen. Sie können auch mit SCM | Update To | Revision… eine bestimmte Fassung wieder herstellen. Wählen Sie einmal in der Projektleiste main.m aus und lassen 819
SmartBooks
Objective-C und Cocoa – Band 2
Sie sich mit SCM | Update To | Revision… den Werdegang anzeigen. Wählen Sie eine Version. Jetzt ahnen Sie auch, dass es wichtig ist, seine Commit-Texte wirklich gut zu wählen.
Zusammenfassung Die Arbeit in der Gruppe kann zu Konflikten führen. Das hat erst einmal nichts mit Programmieren zu tun. Die Änderung von Sourcetexten lässt sich ganz gut mit der SCM-Integration von Xcode bewerkstelligen. Allerdings sind auch Grenzen vorhanden, vor allem, wenn es um Lokalisierungen geht. Hier arbeitet man lieber von Hand. Wenn einmal gar nichts mehr geht, kann man mit dem Löschen der lokalen Arbeitskopie und einem erneuten Checkout das Projekt wieder »synchronisieren«.
Snapshot Snapshots sind eine von Xcode vorgesehene Zwischenspeicherung eines gesamten Projekts. Sie sind allerdings für kurzfristige Änderungen gedacht und nicht für eine komplette Versionskontrolle. Öffnen Sie sich ein beliebiges Projekt. Es existieren in Xocde zwei Möglichkeiten, Schnappschüsse anzufertigen: Die erste Variante ist rasant schnell und daher für schnelle, kurze Tests geeignet. Hierbei wird kein Kommentar erzeugt und dem Namen das aktuelle Datum hinzugefügt. Probieren Sie es einmal: File | Make Snapshot. Sie können sich aber auch die Snapshotverwaltung mit File | Snapshots anzeigen lassen und dort in der Toolbar mit Make einen neuen Schnappschuss erzeugen und gleich auch Kommentare und Namen vergeben. Ändern Sie eine Datei in Ihrem Projekt (speichern!), dann können Sie mit dem Item Files in der Werkzeugleiste anschauen, welche Dateien seit dem Schnappschuss geändert wurden. Der Rest sollte sich von selbst erklären.
Testen Wenn Sie nachts um 4.00 Uhr vor dem Ausliefertermin Ihre Software wenigstens soweit fertig gestellt haben, dass sie halbwegs die versprochenen Features enthält, ist es die richtige Zeit, ein Bett aufzusuchen.
820
Kapitel 9
Auslieferung, Versionierung, Konfiguration
Nö, jetzt muss die Anwendung noch ausgiebig getestet werden. Wir wollen hier einen kurzen Einblick in zwei wichtige Werkzeuge geben: Instruments ist ein Tool von Xcode zum Messen verschiedener Eigenschaften der laufenden Software wie Geschwindigkeit und Speicherverbrauch. Unit-Tests sind dagegen maschinell ausführbare Tests.
Instruments Instruments hilft Entwicklern bei der Optimierung der eigenen Anwendung. Die möglichen Optimierungen sind sehr vielfältig. Mit Instruments können etwa sehr einfach Speicherlöcher und Performance-Engpässe ausfindig gemacht werden. Die Anwendung Instruments wurde erstmals mit den Entwicklerwerkzeugen von Mac OS X 10.5 ausgeliefert. Mit Mac OS X 10.5 hat Apple auch erstmals DTrace in das System integriert. DTrace wurde ursprünglich von Sun Microsystems für Solaris entwickelt, um den Kernel und Anwendungen in Echtzeit auf Fehler untersuchen zu können. Die Untersuchung von Anwendungen hinsichtlich ihrer Performance ist kein triviales Unterfangen. Sie kennen die Problematik vielleicht selbst, falls Sie schon versuchten, mit NSLog() die Performance eines Views zu messen. Der naive Ansatz sieht vor, dass man in die Methode -drawRect: einige Male NSLog aufruft und dann die Zeit abliest. Das Problem hierbei ist, dass NSLog() selbst die Performance sehr beeinflussen kann. Dies kann dann dazu führen, dass Ihre Messungen von NSLog() so sehr beeinflusst werden, dass sie keinerlei Aussagekraft haben. Ein ähnlich gelagertes Problem ist der sogenannte top-Effekt. Mit dem Unix-Befehl top können Sie sich anzeigen lassen, welcher Prozess momentan läuft und wie viel Prozent der CPU ihm zugeteilt sind. Man könnte jetzt vermuten, dass man hiermit die CPUVerteilung messen kann. Das Problem ist, dass der top-Prozess selbst auch CPUEinheiten benötigt, um die Messungen durchführen zu können. Der top-Prozess ist oftmals der Prozess, der die CPU am meisten belastet. Dies verfälscht die Messungen natürlich erheblich. DTrace misst wesentlich intelligenter und mit wesentlich weniger Seiteneffekten, die Messungen beeinflussen könnten. Normalerweise wird DTrace über das Werkzeug dtrace benutzt. Dies ist aber recht kompliziert und nicht sehr visuell. Daher ist Instruments ein GUI für DTrace. Wir werden exemplarisch demonstrieren, wie Sie mit Instruments Speicherlöcher in Ihrer eigenen Anwendung finden und anschließend beheben können, da dies wohl eine der wichtigsten Optimierungen ist, die ein Entwickler beherrschen sollte. Weitere Möglichkeiten entnehmen Sie bitte der Dokumentation zum Tool.
821
SmartBooks
Objective-C und Cocoa – Band 2
Speicherlöcher finden Wir werden nun mit Xcode eine Anwendung erstellen, die ein Fenster zeigt, in dem sich ein Button befindet. Bei einem Klick auf diesen Button soll eine Actionmethode aufgerufen werden, die mutwillig Speicherlöcher erzeugt. Diese Anwendung werden wir anschließend mit Instruments auf Speicherlöcher hin untersuchen. Erzeugen Sie ein neues Projekt vom Typ Cocoa Application. Nennen Sie das Projekt einfach nur Leak. Öffnen Sie die Datei LeakAppDelegate.h und fügen Sie dort eine Actionmethode namens erzeugeLeaks ein. #import @interface LeakAppDelegate : NSObject { NSWindow *window; } @property (assign) IBOutlet NSWindow *window; - (IBAction)erzeugeLeaks:(id)sender; @end
Speichern Sie diese Datei, öffnen Sie die Datei MainMenu.xib und platzieren Sie auf dem Fenster einen Button. Verbinden Sie die Action des Buttons mit der Actionmethode -erzeugeLeaks:. Nun öffnen Sie die Datei LeakAppDelegate.m und implementieren Sie die Actionmethode. #import "LeakAppDelegate.h" @implementation LeakAppDelegate @synthesize window; - (void)applicationDidFinishLaunching:(NSNotification *)n { } - (IBAction)erzeugeLeaks:(id)sender { NSMutableString* s = [NSMutableString stringWithString:@"ab"]; [s retain]; } @end
822
Kapitel 9
Auslieferung, Versionierung, Konfiguration
HILFE Sie können das Projekt in diesem Zustand als »Leak« von der Webseite herunterladen. Um nun das Projekt mittels Instruments auf Speicherlöcher hin zu untersuchen, wählen Sie einfach im Menü Run | Run with Performance Tool | Leaks aus.
Aus Xcode heraus Instruments starten
Nachdem Sie den Menüpunkt Run | Run with Performance Tool | Leaks ausgewählt haben, sollte Instruments sowie die Anwendung Leaks starten. Klicken Sie nicht gleich auf den Button in der Anwendung, sondern blenden Sie in Instruments die erweiterte Ansicht ein, indem Sie im Menü View | Extended Detail auswählen. In Instruments sollte dann rechts ein weiteres View eingeblendet werden. Im oberen Bereich von Instruments befinden sich aktuell zwei Instrumente. Das obere heißt ObjectAlloc und das untere heißt Leaks. Da uns die Speicherlöcher (engl. leaks) interessieren, klicken Sie bitte auf das Leak.
823
SmartBooks
Objective-C und Cocoa – Band 2
In der Abbildung ist links das Instrument Leaks und rechts die erweiterte Ansicht (Tastenkombination [Befehl] + [E]) umrandet.
Nun können Sie den Button betätigen. Beobachten Sie hierbei die erweiterte Ansicht in Instruments. Nach einigen Sekunden sollten in der erweiterten Ansicht einige Einträge erscheinen. Unter den Einträgen sollte ein Eintrag sein, in dessen erste Zeile -[LeakAppDelegate erzeugeLeaks:] steht.
Ein Speicherloch wird in der erweiterten Ansicht angezeigt.
824
Kapitel 9
Auslieferung, Versionierung, Konfiguration
Machen Sie auf diesen Eintrag einen Doppelklick. Instruments sollte dann Quellcode der Datei LeakAppDelegate.m anzeigen und die Zeile markieren, in der das Objekt erzeugt wurde, welches das Speicherloch erzeugt hat. Das war einfach. So lassen sich auch komplexere Anwendungen leicht auf mögliche Speicherlöcher hin untersuchen.
Ausblick Sie haben gesehen, wie einfach sich Speicherlöcher mit Instruments finden lassen. Während des Entwicklungsprozesses einer komplexeren Anwendung sollten Sie sich hin und wieder die Zeit nehmen und mit Instruments nach Speicherlöchern Ausschau halten. Instruments kann Ihnen allerdings noch bei ganz anderen Problemen helfen. Falls Sie in Ihrer Anwendung beispielsweise Gebrauch von Core Data machen und feststellen, dass hier und da Core Data-spezifische Performanceprobleme auftauchen, so können Sie mit Instruments die Zeit messen, die Core Data zum Beispiel zum Speichern benötigt.
Automatisiertes Testen Testen Sie Ihren Quellcode intensiv auf Fehler? Haben Sie hierfür einen Automatismus? Sollten Sie auch nur bei einer dieser beiden Fragen mit Ihrem Kopf geschüttelt haben, so möchten wir Sie zum Weiterlesen ermutigen. In der Regel können schon kleine Änderungen im Quellcode zu vielen (logischen) Fehlern führen. Theoretisch müssten Sie bei jeder Änderung die restlichen Teile Ihres Quellcodes testen, um Fehler, die sich aus der Änderung ergaben, zu finden. Dies ist ein sehr mühsames Geschäft. Wenn Sie sich allerdings die Mühe machen und manuell, nach jeder Änderung, Ihren gesamten Quellcode testen, so ergibt sich ein neues Problem: Beim manuellen Testen können sich ganz schnell Fehler einschleichen. Diese Umstände sind natürlich nicht akzeptabel. Glücklicherweise kann man mit Xcode Testfälle erzeugen, die dann automatisch bei jedem Kompiliervorgang ausgeführt werden. Entdeckt ein Testfall einen Fehler, so erzeugt Xcode eine entsprechende Fehlermeldung. Wir möchten Ihnen nun zeigen, wie Sie mit Xcode derartige Testfälle erzeugen, um so beispielsweise Ihr eigenes Framework zu testen.
Einen Testfall erzeugen Auf der Webseite des Buches finden Sie ein Projekt, welches »CalculatorKit 1« heißt. Dieses Projekt ist ein Cocoa-Framework, welches lediglich die Klasse Calculator enthält. In der Klasse Calculator ist lediglich die Methode +multiply:with: definiert, die das Produkt zweier Zahlen berechnet. Das Framework enthält keinerlei Testfälle. Da Sie dies nun ändern sollen, bitten wir Sie, sich das Projekt namens »CalculatorKit 1« von der Webseite des Buches zu laden und es mit Xcode zu öffnen. Bevor ein Testfall erzeugt werden kann, muss in Xcode ein neues Target hinzuge825
SmartBooks
Objective-C und Cocoa – Band 2
fügt werden. Wählen Sie hierzu im Menü Project | New Target... aus. Als Template wählen Sie Unit Test Bundle. Nennen Sie das neue Target CalculatorKitTests. Dem neuen Target werden wir auch gleich einen Testfall hinzufügen. Allerdings muss dem Target zunächst das Framework CalculatorKit als direkte Abhängigkeit definiert werden. Wenn Sie später dann das Target CalculatorKitTests kompilieren, so sorgt Xcode dafür, dass bei Bedarf das Framework CalculatorKit automatisch kompiliert wird, so dass die Testfälle immer mit der aktuellen Version des Frameworks arbeiten. Wählen Sie hierzu das Target CalculatorKitTests aus und öffnen Sie den Inspektor. Im Bereich General fügen Sie das CalculatorKit als neue Direct Dependency hinzu. Jetzt sollten Sie noch CalculatorKitTests zum aktiven Target machen. Wenn Sie nun an CalculatorKit eine Änderung vornehmen und Xcode den Befehl zum Kompilieren geben, so wird Xcode zunächst das CalculatorKit kompilieren und dann das Unit Test Bundle. Da ein leeres Unit Test Bundle recht langweilig ist, fügen Sie bitte einen Testfall hinzu. Wählen Sie hierzu im Menü File | New File... aus. Als Template wählen Sie Objective-C test case class aus. Nennen Sie den Testfall CalculatorTests. Bevor Sie den Dialog bestätigen, müssen Sie in der Tabelle einzig und allein bei CalculatorKitTests einen Haken setzen. Dies bewirkt, dass der Testfall zum richtigen Target hinzugefügt wird. Ein Testfall ist nichts weiter als eine Klasse, die von SenTestCase ableitet. Der hinzugefügte Testfall ist nicht sonderlich spannend, da er keinerlei Testlogik enthält.
HILFE Sie können das Projekt in diesem Zustand als » CalculatorKit 02« von der Webseite des Buches herunterladen.
Einen Test erzeugen Ein Test ist im Prinzip lediglich eine Methode, die mit dem Präfix test beginnt, keinen Rückgabewert hat und keine Argumente erwartet. Beim Ausführen des Testfalles werden automatisch alle Methoden, die diesen drei Kriterien entsprechen, ausgeführt. Wir möchten nun für die Klasse Calculator einen Test schreiben. Öffnen Sie die Datei CalculatorTests.m und passen Sie den Code wie folgt an: #import "CalculatorTests.h" #import "Calculator.h" @implementation CalculatorTests - (void)testMultiplyer { NSUInteger r = [Calculator multiply:5 with:5];
826
Kapitel 9
Auslieferung, Versionierung, Konfiguration
STAssertTrue(r == 25, @"Multiplizieren schlägt fehl"); } @end
Mit Hilfe der Klasse Calculator lassen wir eine Multiplikation (fünf mal fünf) durchführen. Anschließend wird mittels STAssertTrue – ST steht für Sen Test – überprüft, ob das Ergebnis gleich 25 ist. Falls das Ergebnis nicht dem Wert 25 entspricht, so wird beim Kompilieren der Fehler »Multiplizieren schlägt fehl« angezeigt. Neben STAssertTrue gibt es noch viele weitere Möglichkeiten, Ergebnisse zu überprüfen und Fehler zu erzeugen. Einige wären STAssertNotNil(Objekt, Fehlerbeschreibung, ...), STAssertFalse(boolscher-Ausdruck, Fehlerbeschreibung, ...), STAssertEqualObjects(ObjektA, ObjektB, Fehlerbeschreibung, ...) und STAssertEquals(ObjecktA, ObjektB, Fehlerbeschreibung, ...). Bevor Sie den Testfall kompilieren, müssen Sie noch das Framework CalculatorKit in die Build-Phase Link Binary With Libraries kopieren.
HILFE Sie können das Projekt in diesem Zustand als »CalculatorKit 3« von der Webseite des Buches herunterladen. Wenn Sie nun den Testfall kompilieren, so sollten keine Fehlermeldungen erscheinen. Ändern Sie nun die Implementierung von +multiply:with: (Calculator) ab, um einen Fehler zu provozieren. #import "Calculator.h" @implementation Calculator + (NSUInteger)multiply:(NSUInteger)lh with:(NSUInteger)rh { return lh * rh + 1337; // Fehler provozieren } @end
Kompilieren Sie den Testfall erneut. Xcode sollte das Kompilieren verweigern und eine Fehlermeldung anzeigen.
827
SmartBooks
Objective-C und Cocoa – Band 2
Zusammenfassung Sie haben hier Einblick in wichtige Technologien gewonnen, die rund um die Problematik der fortlaufenden Softwareentwicklung angeordnet sind. Natürlich kann hier nicht – insbesondere im Hinblick auf die Möglichkeiten von Xcode – die Tiefe erreicht werden, die wirklich effizientes Arbeiten ermöglicht. Wir denken aber, dass Sie Einblick in alle wichtigen Expertenwerkzeuge erhalten haben. Von hier aus müssen wir Sie dann in die raue Welt entlassen …
828
Objective-C-Referenz
10
Die Referenz erfasst die wesentlichen Eigenschaften der Programmiersprache Objective-C, aber auch – damit abweichend vom knappen Titel – Grundlagen von Cocoa, ohne die Objective-C nicht sinnvoll einsetzbar ist, wie etwa Initialisierung. Außerdem wird das Laufzeitsystem skizziert.
SmartBooks
Objective-C und Cocoa – Band 2
(Sprach-)Versionen Die nachfolgenden Erläuterungen gehen von Objective-C 2.0 aus. Darüber hinaus existieren Unterschiede zwischen der 32-Bit- und der 64-Bit-Version von Objective-C. Dies mag zunächst verwunderlich erscheinen, da Sprachen eigentlich nicht von dem Architekturmodell abhängen. Und dies ist auch in der Tat gar nicht der Fall. Denn die „iPhone-Version“ von Objective-C ist 32-bittig und verhält sich wie die 64-Bit-Version auf dem Mac. Vielmehr liegt der Grund in der Laufzeitmaschine, die unterhalb des Objective-C-Programmes läuft: Einige neue Features von Objective-C 2.0 verlangen nämlich nach einer anderen Laufzeitmaschine. Diese ist dann aber binär inkompatibel zu bisherigen Objective-C-Programmen. Deshalb war es Apple dort verschlossen, diese neuen Errungenschaften anzubieten, wo bereits kompilierte Programme existieren. Und das sind nun einmal 32-Bit auf dem Mac. Daher scheiden sich die Geschlechter nach alt – 32-Bit Mac – und neu – 64-Bit Mac und 32-Bit iPhone. Apple nennt in der Dokumentation die 64-Bit-Version für den Mac und damit 32-Bit-Version für das iPhone auch »modern runtime«. Wir werden an den entsprechenden Stellen darauf zurückkommen.
Einfache Typen Sowohl in C als auch in Objective-C, teilweise sogar erst durch Cocoa, werden einige einfache Typen festgelegt.
Ganzzahlen Zunächst können ganzzahlige Werte dargestellt werden, wobei auch Zeichen über ihren ganzzahligen Code behandelt werden:
char, short, int, long, long long, size_t, fpos_t, off_t Ganzzahlen werden zunächst durch die C-Typen char, short, int, long und long long dargestellt. Ihnen kann das Wort »unsigned« vorangesetzt werden, um sie als vorzeichenlose Typen darzustellen, deren Bereich bei 0 beginnt. Bei dem Typen char kann man per Compileroption (Build-Optionen | GCC 4.0 – Language | 'char' Type Is Unsigned) einstellen, ob es sich ohne Zusatzangabe um ein vorzeichenbehaftetes Format oder eines ohne Vorzeichen handeln soll. Wenn ein vorzeichenloses Format verwendet wird, so muss man für eine einzelne Definition mit Vor830
Kapitel 10
Objective-C-Referenz
zeichen das Schlüsselwort »signed« voransetzen. Dies kann man freilich stets, es ist jedoch ohne Wirkung, wenn der Typ ohnehin bei Fehlen einer ausdrücklichen Angabe vorzeichenbehaftet ist: signed int a = -5;
Ferner existiert der Typ size_t, der als Ergebnis geliefert wird, wenn mittels des Operators sizeof() die Größe einer Variablen abgefragt wird. long a = …; size_t sizeInBytes = sizeof( a ); // Liefert 4 fuer 4 Bytes (32 Bit) NSLog( @"%d", sizeInBytes );
Ebenfalls noch aus C (nämlich stdio.h) stammen die Typen fpos_t und off_t, die sich auf Dateipositionen beziehen.
BOOL, YES, NO Da C selbst keinen Standardtypen für boolsche Ausdrücke definiert, holt dies Objective-C mit BOOL nach. Auch wenn dies als Ganzzahl implementiert ist, so sind die einzig zulässigen Werte für eine Variable diesen Typs YES und NO. Da eine C-Bedingung indessen das Wertepaar 0/Nicht-0 hat, handelt es sich um einen anderen Typen. Allerdings sind diese für Bedingungen austauschbar definiert.
NSInteger, NSUInteger Dies sind insbesondere NSInteger und NSUInteger für vorzeichenbehaftete bzw. vorzeichenlose Ganzzahlen, was in NSObjCRuntime.h definiert wird. Nach der unten dargestellten Tabelle dienen sie vor allem der Anpassung der ganzzahligen C-Typen an eine 32-Bit- bzw. 64-Bit-Umgebung.
unichar, FourCharCode Außerdem legt NSString.h den Typen unichar an, der für Zeichen im Unicode verwendet werden soll. Es handelt sich dabei um den unmittelbaren Unicode-Index ohne UTF-8- oder UTF-16-Kodierung. In MacTypes.h findet sich ab OS X 10.5 ein zusätzlicher Typ FourCharCode, der dafür verwendet werden soll, wenn etwa der Dokumententypcode aus vier Zeichen abgefragt wird: FourCharCode fourLetter2 = 'Negm';
Literale Konstanten Literale Konstanten sind Zahlen, die sich unmittelbar im Sourcecode befinden. Grundsätzlich werden sie als vorzeichenbehafteter Integer (signed int) interpre831
SmartBooks
Objective-C und Cocoa – Band 2
tiert. Man kann jedoch durch Hinzufügung von Typangaben hinter dem Typen oder Typangabe vor der literalen Konstante bestimmte Typen erzwingen. Dies ist erforderlich, wenn der Compiler nicht selbst den Typen ermitteln kann, weil er sich etwa erst aus dem Formatstring ergibt: // Funktioniert nicht, weil sich der Long-Long-Wert 20000000000 // nicht als int darstellen lässt: NSLog( @"%lld", 20000000000 ); // Explizite Angaben NSLog( @"%lld", 20000000000LL ); NSLog( @"%lld", (long long)20000000000 );
Die Codes für einzelne Typen entnehmen Sie bitte der nachfolgenden Tabelle. Neben der dezimalen Schreibweise von literalen Konstanten existiert eine hexadezimale und eine oktale. Die hexadezimale Darstellung wird durch 0x eingeleitet, die oktale durch 0. Bei der hexadezimalen Darstellung spielt es keine Rolle, ob die Ziffern A-F groß oder klein geschrieben werden: int value = 0xbabe; // Hexadezimale Zahl babe; value = 03352; // Oktale Zahl 3352
Daneben existiert die Möglichkeit, Ganzzahlen als Zeichencodes (ASCII) einzugeben. Dies funktioniert für 1-Byte- und 4-Byte-Zeichenketten: char c = 'A'; int i = 'Amin';
Speicherplatz und Wertebereiche Jedem Typen wird ein gewisses Maß an Speicherplatz eingeräumt, aus dem sich dann der darstellbare Wertebereich ergibt. Grundsätzlich gilt, dass ein vorzeichenloser (unsigned) Typ Werte von 0 bis zu 2 hoch Anzahl seiner Bits abzüglich 1 darstellen kann: UMAX = (2 ^ Anzahl der Bits) - 1 Für vorzeichenbehaftete (signed) Zahlen gilt, dass der negative Bereich betragsmäßig der Hälfte entspricht, der positive zusätzlich um 1 vermindert ist. SIGNED_MIN = 2 ^ (Anzahl der Bits - 1) SIGNED_MAX = (2 ^ (Anzahl der Bits - 1)) - 1
832
Kapitel 10
Objective-C-Referenz
Diese äußeren Grenzen für die verschiedenen Typen sind in limits.h vordefiniert und sind für Typen ohne Vorzeichen mittels der Definitionen UTYPPRÄFIX_MAX festgelegt. Wollen Sie den Wertebereich eines vorzeichenbehafteten Typen kennen, verwenden Sie TYPPRÄFIX_MIN bzs. TYPPRÄFIX_MAX. unsigned long long veryBig = … unsigned int regular; if( veryBig zugreifen.
Unionstyp union Unions sehen den Strukturen ganz ähnlich, speichern aber stets nur eine von vielen Komponenten. union MyUnion { int intValue; char aChar; }; union MyUnion check; check.intValue = 98; check.aChar = 'T';
Der – fehleranfällige – Witz dabei ist, dass keinerlei Überprüfung erfolgt, was zuletzt gespeichert wurde. Vielmehr wird die jeweils andere Komponente einfach überschrieben. // liefert den "Unicode" von 'T' (=84), nicht 98: NSLog( @"%d", check.intValue );
Ebenso schon pathologisch Fehler herausfordernd ist es, eine Union-Variable in der Definition zu initialisieren. Man muss dann in geschweiften Klammern einen Wert angeben. Hier ist dann nicht ersichtlich, welche Komponente zugewiesen wird und welche Typumwandlungen dabei gegebenenfalls vorgenommen werden: union Person { NSInteger size; float age;
843
SmartBooks
Objective-C und Cocoa – Band 2
}; union Person personVar = { 7.2 }; NSLog( @"%d %f", personVar.size, personVar.age );
C-Arrays Nur äußerst selten sollten Sie sogenannte C-Arrays verwenden. Hierbei handelt es sich um eine »stumpfe« Vervielfachung einer Variablen. So wird etwa mit NSInteger fibonacci[50];
in der Variablen fibonacci Speicherplatz für 50 Werte des Typs NSInteger angefordert. Auch diese lassen sich unmittelbar initialisieren. Hierbei werden wie bei einer Struktur die Werte in geschweiften Klammern geschrieben, wobei auch teilweise Initialisierungen möglich sind: NSInteger fibonacci[50] = { 1, 2, 3, 5, 8 };
Auf die einzelnen Komponenten eines Arrays greift man mit NSInteger normalo = fibonacci[23]; fibonacci[9] = 811;
zu, wobei auch hier die Zählung mit 0 beginnt, so dass sich die letzte Komponente in fibonacci[49] befindet. Man kann mehrdimensionale Arrays bauen (in C handelt es sich nur um einen Trick), indem man die eckigen Klammern wiederholt. NSInteger chessboard[8][8]; chessboard[2][3] = 5;
Bei der Initialisierung sind dann verschachtelte geschweifte Klammern zu verwenden, wobei sowohl die inneren als auch das äußere Array unvollständig initialisiert werden können: NSInteger chessboard[8][8] = { { 5, 8, 3 }, { 7 }, { 1, 2, 3, 4 } };
844
Kapitel 10
Objective-C-Referenz
Wenn man eine Initialisierung hat, ist es zulässig, die Größe einer Dimension nicht anzugeben. Es werden dann die Anzahl der Werte aus der Initialisierung genommen. NSInteger row[] = { 3, 4, 5 }; // 3 Elemente NSInteger row[4] = { 3, 4, 5 }; // 4 Elemente, 3 initialisiert
C-Arrays sind anders als Strukturen und Unions namenlos, wie Sie sehen können. Wie jedoch auch die anderen komplexen Datentypen lassen sie sich bei einer gesonderten Typdefinition benennen. Aber auch in anderer Hinsicht sind sie etwas Besonderes. So lassen sich Arrays nicht zuweisen, auch dann nicht, wenn sie über dieselbe Anzahl an Dimensionen und Größe verfügen: NSInteger position1[8][8]; chessboard[2][3] = 5; NSInteger position2[8][8]; chessboard = position1; // Fehler!
Daraus folgt auch, dass sie sich nicht als Parameter übergeben lassen. Hintergrund ist, dass das Array ohne Index ein Zeiger auf das erste Element ist. Man kann daher das Array an einen solchen Zeiger zuweisen. Vorsicht: Hierbei wird nicht etwa das Array an die Stelle kopiert, auf die der Zeiger referenziert. (Man kann auch formulieren: Die Zuweisung erfolgt By-Reference und nicht By-Value, wie bei Strukturen und Unions.) Vielmehr wird der Zeiger auf den Beginn des Arrays gesetzt. double* doublePointer; double doubleArray[3]; doublePointer = doubleArray; // ohne eckige Klammern. int arrayA[3]
arrayA 0: … 1: … 2: …
int arrayB[5]
arrayB 0: … 1: … 2: … 3: … 4: …
int* element
Ein Array ist ein Speicherbereich, ein Zeiger verweist auf irgendwas.
845
SmartBooks
Objective-C und Cocoa – Band 2
Wie Sie der Graphik entnehmen können, kann dabei ein Zeiger auf jede Speicherstelle verweisen, also auf das erste Element eines 3er-Arrays, auf das erste Element eines 5er-Arrays und auf jedes beliebige Element innerhalb des Arrays: int arrayA[3]; int arrayB[5]; int* element; // element zeigt auf ... element = arrayA; // ... (das erste Element von) arrayA element = arrayB; // ... (das erste Element von) arrayB element = &arrayA[1]; // ... das zweite Element von arrayA
Der Unterschied zwischen Arrays und Zeigern liegt letztlich darin, dass bei Arrays der Compiler für ausreichend Speicher im Array sorgt, während bei Zeigern der Programmierer diesen Speicher besorgen muss. Bei mehrdimensionalen Arrays weiß der Compiler zudem, in welcher Schrittweite er im äußeren Array von Element zu Element kommt. Noch ähnlicher sind sich offene Arrays und Zeiger bei offenen Arrays, die als Parametertyp angegeben werden. Dort ist es nämlich erlaubt, die äußerste Dimension offen zu lassen: void function( NSInteger row[] ) { row[3] = 1; }
Da der Compiler die äußere Definition nicht kennen muss, um auf ein Element zuzugreifen – die Schrittweite errechnet sich ja aus den inneren –, gilt das auch für mehrdimensionale Arrays: void function( NSInteger row[][5] ) { row[3][2] = 1; }
Allerdings ist es auch bei Arrays so, dass der Compiler nicht überprüft, ob der Speicher tatsächlich ausreicht, und zwar unabhängig davon, ob das Array in seiner Größe in allen Dimensionen spezifiziert wurde oder nicht. So führt int array[5]; array[98] = 11;
846
Kapitel 10
Objective-C-Referenz
nicht zu einer Fehlermeldung bei der Übersetzung des Programmes – und auch zur Laufzeit nicht. Das kann dazu führen, dass an ganz anderer Stelle Ihres Programmes sich auf einmal Daten verändern bis hin zum Komplettabsturz des Programmes viel später und an einer ganz anderen Stelle.
Typdefinitionen Es ist in den C-Sprachen möglich, neue Datentypen zu definieren. Damit sind nicht die vorstehenden komplexen Typen gemeint, sondern deren Benennung mittels eines einfachen Bezeichners, wie dies auch bei den eingebauten Typen ist. Sie kennen bereits zwei Beispiele aus dem ersten Band: NSInteger und NSUInteger, die wie folgt definiert sind: #if __LP64__ || NS_BUILD_32_LIKE_64 typedef long NSInteger; typedef unsigned long NSUInteger; #else typedef int NSInteger; typedef unsigned int NSUInteger; #endif
Diese Definition sieht in der Tat ähnlich wie eine Variablendefinition aus. Bemerkenswert ist das Schlüsselwort »typedef« am Anfang. Es kennzeichnet eine nachfolgende Typ- anstelle Variablendefinition. Wie bei der Definition von Variablen folgt der Typ, bei uns int. Als Letztes muss jedoch der Bezeichner notiert werden, unter dem der Typ bekannt sein soll. Dies ist vor allem für Hersteller von Systemframeworks wie Apple interessant, da so eine gewisse Abstraktion eingebaut werden kann. Aber man kann auch selbst die vorbesprochenen komplexen Datentypen damit in der Handhabung vereinfachen. Auf die obige Struktur angewendet: typedef struct _Person { NSInteger size; NSInteger age; } Person;
Nach der Typdefinition ist Person ein neuer Datentyp, mit dem man Variablen anlegen kann, wie das in Cocoa etwa mit NSSize erfolgt ist:
847
SmartBooks
Objective-C und Cocoa – Band 2
Person aPerson; person.size = 185;
Selbstverständlich kann dieser Typ auch wie ein eingebauter Typ modifiziert werden, etwa ein Zeiger hierauf angelegt werden: Person* pointerToPerson;
oder wieder in einer Struktur verwendet werden – auch wenn diese wiederum in einer Typdefinition liegt: typedef struct { Person husband; Person wife; } Pair;
Es ist auch erlaubt, den komplexen Typen selbst zu benennen. Häufig ist dies nicht notwendig, wenn er gleich durch typedef läuft. Es ist dabei sogar erlaubt, dass der komplexe Typ den gleichen Namen trägt, wobei man allerdings als Hinweis traditionell einen Unterzug voranstellt: typedef struct _Person { NSInteger size; NSInteger age; } Person; struct _Person wife; Person husband;
Auch Arrays lassen sich als typedef lesbarer gestalten. Allerdings wird dadurch die Größe verschwiegen, was wiederum ob der mangelnden Überprüfung gefährlich erscheint: typedef NSInteger Row[8]; Row oneRow; oneRow[4] = 3;
Gleiches gilt für Zeiger: typedef NSInteger* IntegerPointer; NSInteger value = 4; IntegerPointer pointer = &value; *pointer = 3;
848
Kapitel 10
Objective-C-Referenz
Speicherklassen und Qualifizierer Man kann in C einer Variablen eine sogenannte Speicherklasse und Qualifizierer zuweisen. Sie hatten die wichtigsten Fälle auch schon im ersten Band kennengelernt. Schauen wir uns das noch etwas genauer an.
const Mit dem Qualifizierer const bestimmen Sie, dass eine Variable unveränderlich ist. An sie kann also kein neuer Wert zugewiesen werden. Daraus folgt sogleich, dass bei der Anlage ein Wert zugewiesen werden muss, wenn die Variable einen Sinn haben soll. const int rowCount = 8;
Wird also irgendwo im Programm rowCount verwendet, so beträgt der Wert stets 8. Auch hier verhält es sich ähnlich wie bei enum, dass es um den symbolischen Ausdruck einer Zahl geht. Es gibt aber einen entscheidenden Unterschied. Schauen wir uns das einmal in der Gegenüberstellung an:
•
Konstanten können als »Klartext« wie 5 oder 92.336 usw. unmittelbar in den Code getippt werden. Dies hat den Nachteil, dass keine Bezeichnung erfolgt und die Bedeutung nur schwer zu erraten ist. Außerdem lässt sich der Wert nachträglich nur schwer ändern, da die Stellen unbekannt sind.
•
Konstanten können symbolische Namen erhalten, wenn sie in einem Aufzählungstypen (Enum) genannt werden.
•
Konstante Variablen sind dagegen echte Variablen, bei denen lediglich der Compiler die Zuweisung verbietet. Sie belegen Speicher – was angesichts der heutzutage verbauten Menge als Nachteil vernachlässigbar ist. Deshalb lässt sich die Adresse einer konstanten Variable ermitteln.
Marker werden in Objective-C häufig derartig definiert. In Band 1 hatten wir das etwa für Pasteboard-Definitionen gemacht. Dabei verwendet man einen Zeiger auf einen String. NSString* const MyMarker = @"com.cocoading.myMarker";
Bei einem Zeiger stellt sich übrigens die Frage, was konstant sein soll: Der Zeiger selbst oder das, worauf er zeigt. In dem obigen Beispiel wird der Zeiger als konstant vereinbart. Dies ist daran zu erkennen, dass das const erst nach dem Sternchen erfolgt. Der Zeiger lässt sich also nicht mehr verändern. Grundsätzlich wäre das, worauf der Zeiger verweist, aber abänderbar. Nur handelt es sich ja um eine String-Konstante von Objective-C. Die bestimmt selbst, ob sie veränderlich ist – ist sie nicht. 849
SmartBooks
Objective-C und Cocoa – Band 2
GRUNDLAGEN Grundsätzlich kann der Hinweis, dass eine verwiesene skalare Variable nicht geändert werden kann, nützlich sein. Zum einen führt dies zu mehr Sicherheit, zum anderen kann ein Compiler hieraus Schlüsse für die Optimierung ziehen. In Objective-C sind ohnehin konstante Zeiger auf Instanzen nur bei der Klasse Strings möglich, da sie sich nur in der Konstantendefinition erzeugen lassen. Damit ist das verwiesene Objekt stets konstant.
Lokales static und auto Werden Variablen innerhalb eines Blockes definiert, so können sie nur innerhalb des Blockes angesprochen werden. Man spricht insoweit von »lokalen« Variablen. Dies gilt insbesondere für Variablen, die in einem Methodenrumpf definiert werden: - (void)doSomething { // Blockbeginn int localVar; … } // Blockende
Diese lokalen Variablen sind nur innerhalb des umschließenden Blockes benutzbar und werden beim Verlassen des Blockes automatisch zerstört, ihr Inhalt wird gelöscht. Wird also dieselbe Methode erneut aufgerufen, so steht in der Variablen ein neuer zufälliger Wert oder der Initialisierungswert, falls wir einen angeben. Der alte Inhalt ist jedenfalls verloren: - (void)doSomething { int zufall; // Keine Initialisierung: Zufaelliger Wert int sicher = 3; // Initialisierung: sicher ist garantiert 3 … }
Bei genauerer Betrachtung fällt jedoch auf, dass die Lokalität der Variablen zwei Eigenschaften betrifft, die sich gedanklich trennen lassen. Man bezeichnet sie als »Scope« und »Extent«:
•
Mit »Scope« oder »Sichtbarkeit« ist der Umstand gemeint, dass man auf eine Variable, die innerhalb eines Blockes definiert ist, nur innerhalb dieses Blockes (einschließlich Subblöcken innerhalb des Blockes) zugreifen kann. Außerhalb des Blockes ist sie unbekannt. 850
Kapitel 10
Objective-C-Referenz
•
»Extent« bezeichnet indessen die Lebensdauer einer Variablen, also den Zeitpunkt zu den ihr Inhalt gelöscht wird. Grundsätzlich ist dies bei einer Variablen, die in einem Block definiert wird, ebenfalls der Block.
Bei den meisten Programmiersprachen ist damit alles gesagt: Sie behandeln Scope und Extent deckungsgleich. Nicht jedoch bei C und damit Objective-C: Hier ist es möglich, eine Variable mit lokaler Sichtbarkeit, jedoch globaler Lebensdauer anzulegen. Einfacher gesagt: Wenn Sie eine solche Variable in einer Methode anlegen und einen Wert zuweisen, dann behält sie diesen auch nach Verlassen der Methode noch. Wird die Methode erneut ausgeführt, so steht in der Variablen noch der alte Wert. Eine etwaige Initialisierung der Variablen erfolgt nur bei dem ersten Durchlauf der Methode. Gleiches gilt freilich für Funktionen. Das Gegenteil bildet die Speicherklasse auto, welche dafür sorgt, dass die Variable beim Blockeintritt angelegt und beim Verlassen des Blockes weggeworfen wird. Da dies der Standard ist, schreibt man den Qualifizierer üblicherweise nicht. Wir haben ihn bisher stets weggelassen. @interface MoreAboutC : NSObject {} - (void)halfStatic; @end @implementation MoreAboutC - (void)halfStatic { auto int forget = 1; // auto ist Default static int remember = 1; NSLog( @"remember %d times", remember ); NSLog( @"forget %d times", forget ); remember++; forget++; } @end
{ forget=0; static remember=0; forget++; remember++; }
forget=0 remember=0 forget=1 remember=1 forget=1 remember=1
forget=0 remember=1 forget=1 remember=2 forget=1 remember=2
Erste Ausführung
{ forget=0; static remember=0; forget++; remember++; }
Zweite Ausführung
static erhält den Wert einer lokalen Variablen.
851
SmartBooks
Objective-C und Cocoa – Band 2
Beim ersten Aufruf werden forget und remember mit 1 initialisiert, ausgegeben und dann erhöht. Bei jedem weiteren Aufruf wird forget wiederum auf 1 initialisiert, remember indessen behält den letzten Wert, ist also beim zweiten Aufruf 2, beim dritten 3 usw. Wichtig ist das bei Klassenmethoden: Man kann also auf diese Weise einmalige Klassenvariablen erzeugen - sogenannte Singletons. Dabei handelt es sich um Klassen, die nur eine Instanz kennen. +sharedApplication (NSApplication) ist etwa so ein Fall. + (SingletonClass*)sharedInstance { static instance = nil; // Erster Aufruf? Dann erzeugen if( instance != nil ) { // Erzeugung nur beim ersten Aufruf instance = [[SingletonClass alloc] init]; } return instance; }
Sie sehen hier also, dass, wenn die Methode das erste Mal ausgeführt wird, instance mit nil initialisiert wird. Dies führt zur Ausführung des If-Zweiges, wodurch eine Instanz angelegt wird. Wegen des static bleibt es auch bei dieser einen Instanz. Bei zukünftigen Aufrufen scheitert das If, so dass immer wieder die einmal erzeugte Instanz zurückgeliefert wird.
TIPP Da es auch noch andere Methoden wie -copy gibt, die Instanzen erzeugen, müssen auch diese bei einem Singleton überschrieben werden. In der Dokumentation von Apple existiert dazu eine vollständige Implementierung. Diese verwendet allerdings eine modullokale Variable.
Globales static Die Speicherklasse static hat eine weitere Bedeutung wenn sie außerhalb eines Blockes steht. Eine solche Variable nennt man »globale« Variable. Sie wird ja durch nichts eingegrenzt. Dies bedeutet, dass sie grundsätzlich von jedem beliebigen Programmteil aus benutzt werden kann. Das obige Beispiel mit der String-Konstante ist etwa so ein Fall. 852
Kapitel 10
Objective-C-Referenz
Modul1 int global; static int modGlobal; …
Modul2 int global; static int modGlobal; …
static trennt auf Modulebene Variablen.
Man kann das übrigens recht leicht testen: Wenn Sie in einem Modul eine globale Variable anlegen und dies noch einmal in einem anderen Modul machen, dann existiert diese nur ein Mal. Sie kann also aus beiden Modulen geändert werden, ohne dass der andere das bemerkt. Deshalb ist es auch unzulässig, diese Variable in mehreren Modulen zu initialisieren. Dies zeigt jedoch, dass globale Variablen für den praktischen Einsatz völlig unbrauchbar sind: Wenn Sie eine solche definieren, können Sie niemals wissen, ob nicht irgendwo anders eine ebensolche Variable definiert wurde. Beide würden sich dann aber gegenseitig beeinflussen – ohne jede Kontrolle. Man muss also dem Compiler explizit mitteilen, dass es sich bei den beiden Variablen um verschiedene handelt, weil sie in verschiedenen Dateien stehen. Und hierzu dient der globale Einsatz des Schlüsselwortes static. Setzen Sie in beiden Modulen der globalen Variable diese Speicherklasse voran. static int anInteger;
Jetzt erzeugt der Compiler tatsächlich zwei Variablen gleichen Namens. Welche verwendet wird, bestimmt sich nach dem Modul, da der Scope entsprechend beschränkt ist. Die Gefahr der versehentlichen Veränderung beschränkt sich also jetzt auf das Modul und ist daher deutlich reduziert.
extern Bei einer globalen Variable bedeutet extern, dass eine Definition der Variablen in einem anderen Modul erfolgt, der aktuelle Code jedoch nur ein Hinweis darauf ist, eine sogenannte Deklaration. Wie bereits im ersten Band durchexerziert, lassen sich damit insbesondere Marker über Modulgrenzen hinweg publizieren.
Modul1 extern int extVar; static int modVar; …
Modul2 int global; static int modVar; …
Die extern-Deklaration verweist auf Variablen in anderen Modulen.
853
SmartBooks
Objective-C und Cocoa – Band 2
GRUNDLAGEN Auch extern hat eine zweite Bedeutung, wenn es innerhalb eines Blockes erfolgt: Eine globale Variable wird zur Benutzung angezeigt. Dies ist aber zum einen nicht notwendig, da globale Variablen ohnehin in einer Methode benutzbar sind. Zum anderen wollen wir keine globalen Variablen mehr verwenden. Zu diesem Zwecke definiert man die Variable in der Implementierung ganz normal. NSString* aMarker = @" Software #9811.product.marker";
Dem Header fügt man eine Wiederholung dieser Definition hinzu, wobei man allerdings extern voransetzt. Dadurch wird beim Import der Headerdatei in ein anderes Modul nicht jedes Mal eine neue Variable erzeugt, sondern die alte wieder verwendet. Damit ist diese Variable in all den Modulen bekannt, die Modul.h importieren. extern NSString* aMarker;
Aber wir hatten Sie ja vor globalen Variablen gewarnt, weil jeder diese Variable ändern kann. Marker dienen jedoch nur der Identifizierung. Ein solcher Identifier soll sich aber gar nicht ändern. Und deshalb wird ein solcher globale Bezeichner mit einem const erweitert. Dies muss sowohl im Header … extern NSString* const aMarker;
… wie auch in der Implementierung erfolgen: NSString* const aMarker = @"Software #9811.product.marker";
Bei lokalen Variablen bewirkt extern, dass die Sichtbarkeit einer globalen Variable in einem Block übernommen wird. Da dies jedoch stets erfolgt, kann die gesamte Variablendeklaration ausgespart werden.
register Mit register konnte früher festgelegt werden, dass eine Variable zum schnelleren Zugriff in einem Prozessorregister abgelegt werden soll. Heutige Compiler berücksichtigen dies nicht, da ihre Zuteilung der Register in aller Regel eine bessere Optimierung verspricht als die Zuteilung durch den Programmierer.
854
Kapitel 10
Objective-C-Referenz
__block Mit Blocks führte Apple eine Erweiterung des C-Standards (und nicht erst Objec tive-C) ein, die auch zur Standardisierung angemeldet wurde: die Speicherklasse __block. Die Speicherklasse hat mehrere Wirkungen:
•
Von dem definierenden Scope in Blocks vererbte (extern) Variablen werden beschreibbar (by-Reference).
• •
Es erfolgt keine Sicherung verwiesener Instanzen mittels retain. Die Variablen erhalten ihren Extent für alle Ausführungen von verwendenden Blöcken und von ihren definierenden Scopes. Dies bedeutet, dass sie möglicherweise kopiert werden müssen, also ihre Adresse ändern können. Man sollte daher ihre Adresse nicht speichern.
Blocks werden im Abschnitt über Ausführungseinheiten genauer beschrieben.
Typcasting Mit Casting bezeichnet man die explizite Änderung eines Typen. Casting ist sowohl bei Zahltypen möglich als auch bei Zeigern. Im letzten Falle wird es benötigt, um untypisierte Zeiger verwendbar zu machen oder um Objektzeiger zu typisieren. Insbesondere die Typisierung auf eine Instanz einer bestimmten Klasse kann notwendig sein. Das Casting wird durch Voranstellung des Typen in runden Klammern bewerkstelligt, wobei es der Lesbarkeit dient, den »gecasteten« Ausdruck ebenfalls zu klammern: id anInstance = … NSString* aString; aString = ((NSString*)anInstance);
Da beim Casting von Zeigern keine Umwandlung des referenzierten Objektes erfolgt, ist dies eine gefährliche Angelegenheit. Bei Zahlen kann Casting die Anwendung der Operatoren verändern: CGFloat result; result = 5 / 2; // Int-Division: 2 NSLog(@"%f", result ); result = (CGFloat)5 / (CGFloat)2; // Float-Division: 2.5 NSLog(@"%f", result );
855
SmartBooks
Objective-C und Cocoa – Band 2
Bei einer Zuweisung wird automatisch (implizit) gecastet. Gleiches gilt für die Anwendung von Operatoren, wenn die Operanden unterschiedlichen Typs sind.
Ausdrücke Mit Ausdrücken bezeichnet man anschaulich gesagt eine Rechnung. Ausdrücke haben daher stets einen Wert als Ergebnis, welcher etwa als Parameter dienen kann oder zugewiesen wird – was wiederum ein Ausdruck ist. Neben der Verwendung von Konstanten ist der einfachste Ausdruck die Verwendung einer literalen Konstante oder eines Variablennamens, da dies durch den Wert der Variable als Ergebnis ersetzt wird. Beispiele: 5 // Ergebnis: 5 98.11 // Ergebnis: 98.11 integerVar // Ergebnis: Inhalt der Variable
Eine Anweisung kann allein schon eine Anweisung bilden. Dies ist klar, wenn man bedenkt, dass eine Zuweisung ein Ausdruck ist: a = 5;
Das gilt aber für alle Ausdrücke, weshalb 5+3;
eine zulässige Anweisung darstellt.
Operanden Ausdrücke können jedoch mithilfe von Operatoren komplex etwa zu einer Addition zusammengesetzt werden. Die zulässigen Typen der Operanden hängen dabei vom Operator ab. Unabhängig vom Typen stellen literale Konstanten (98.11 usw.), Variablen und Funktionsaufrufe und Nachrichten jeweils mit Rückgabewert zulässige Operanden dar. Ob eine Operation einen (Beispiel: logisches Nicht), zwei (Beispiel: Addition) oder gar drei Operanden (Beispiel: bedingter Ausdruck) benötigt, hängt von der Operation ab. Ein Operand kann nur von einem einfachen Typen sein, nicht jedoch ein Objekt, weshalb sie hier besprochen werden. Allerdings sind auch Objektzeiger taugliche Operanden.
856
Kapitel 10
Objective-C-Referenz
Algebraische Operatoren Komplexere Ausdrücke entstehen durch die Verwendung von Operatoren: 98 + 11
// Ergebnis: 109
Grundrechenarten Es sind die aus der Algebra bekannten Operatoren + (Addition), - (Subtraktion), * (Multiplikation), / (Division) und % (Modulo) anwendbar. Algebraische Operatoren arbeiten grundsätzlich mit der Genauigkeit des Typen. Werden also zwei ganzzahlige Typen angegeben, so ist das Ergebnis ganzzahlig. Wichtig ist dies beim Operatoren für Division, der eine ganzzahlige und damit abgerundete Division vornimmt. Alle Ergebnisse zwischen 0 und 1 werden daher 0: 5 / 2 // Ergebnis: 2 4 / 5 // Ergebnis: 0
Der Modulooperator % benötigt ganzzahlige Operanden. Ansonsten werden alle Zahltypen als Operanden akzeptiert. Vorzeichen Es existieren selbstverständlich die Vorzeichenoperatoren + und -, die sich auf den folgenden Wert beziehen. + ist dabei wirkungslos. Vorzeichen benötigen eine literale Zahlkonstante oder eine Zahlvariable. Inkrementierung- und Dekrementierungssoperatoren Mit ++ und -- lassen sich in einen Ausdruck eingesetzte Werte erhöhen (++) bzw. verringern (--). Sie haben also die Wirkung einer Rechnung mit Ergebnis und einer Zuweisung. Dabei kommt es darauf an, an welcher Stelle der Operator eingesetzt wird: a++ // Ergebnis: a, Wert von a wird a+1 ++a // Ergebnis: a+1, Wert von a wird a+1
Es ist wichtig zu wissen, dass die mehrfache Anwendung dieser Operatoren in einem Ausdruck zu undefinierten Ergebnissen führt: a++ * a // Ergebnis: ?, Wert von a wird a +1
Die Verwendung ist fehleranfällig. Man sollte diese Operatoren nur isoliert anwenden. Die Operatoren benötigen einen lvalue als Operanden.
857
SmartBooks
Objective-C und Cocoa – Band 2
Implizites Casting Sind die Operanden unterschiedlichen Typs, so wird die Operation nach einem Casting des unmächtigeren Typs auf den mächtigeren Typen des anderen durchgeführt. Dies bedeutet die Umwandlung von Typen mit kleinem Wertebereich auf solche mit größerem Wertebereich und gegebenenfalls die Umwandlung von Ganzzahlen in Fließkommazahlen: 4.0 / 5 // Umwandlung von int nach double, entspricht: 4.0 / 5.0 // Ergebnis: 0.8 (double)
Vergleichsoperatoren Vergleichsoperatoren vergleichen zwei Operanden auf ihre Wertigkeit. Es existieren < (Operand 1 ist kleiner), > (größer), = (größer gleich), == (gleich) und != (ungleich). Das Ergebnis ist ein boolscher Ausdruck, der den Wert 0 für falsch und ungleich 0 für wahr erhält. Vergleichsoperatoren für Fließkommazahlen sind gefährlich, weil diese prinzipbedingt ungenau sind und daher Ungleichheit angezeigt werden kann, wo in Wahrheit inhaltlich »Gleichheit mit Rundungsfehler« vorliegt. Man sollt dies daher nur für literale Konstanten verwenden, die explizit gesetzt werden.
Boolsche Operatoren Die Operatoren & (logisches Und), | (logisches Oder) und ! (logisches Nicht mit nur einem Operanden) lassen sich dazu verwenden, boolsche Ausdrücke zu formulieren. Dabei ist jedoch zu beachten, dass in C der boolsche Ausdruck als Zahl interpretiert wird, der entweder 0 (falsch) oder ungleich 0 (wahr) ist. Boolsche Ausdrücke und BOOL Dies ist etwas anderes als der Datentyp BOOL, der die Werte NO und YES verwendet. Da jedoch NO als 0 definiert ist und YES als 1 (also ungleich 0), ist ein weitestgehender Austausch möglich. Dies darf jedoch nicht darüber hinwegtäuschen, dass hier eine heimliche Umwandlung erfolgt. BOOL result = 5 < 9; // implizit BOOL result = (5 < 9)?YES:NO; // explizit
Die erste Zeile setzt jedoch voraus, dass der Compiler für wahr einen Wert liefert, der YES entspricht. Umgekehrt gilt das auch für das Einsetzen von BOOL-Variablen als Bedingung: if( result ) // implizit if( result == YES ) // explizit
858
Kapitel 10
Objective-C-Referenz
Tatsächlich funktionieren die impliziten Umwandlungen. Sie sollten sich aber nicht wundern, wenn Sie in Sourcen die explizite Variante sehen. Boolsche Ausdrücke und nil Ein ähnliches Problem stellt sich bei der Verwendung von Zeigern in boolschen Ausdrücken. Objective-C definiert nil und auch Nil ausdrücklich mit dem Wert 0. Sie sind daher zugleich logisch „falsch“ im Sinne von C. if( if( if( if(
self ) self != nil ) !self ) self == nil )
// // // //
implizit explizit implizit explizit
Bitoperatoren Aus dem Umstand, dass ein Computer für Ganzzahlen einzelne Binärstellen verwendet, lassen sich die einzelnen Bits auch als »Ja« und »Nein« interpretieren und so Fragen zu Blöcken kombinieren. Nehmen wir etwa das Beispiel des Autosizings. Im Interface Builder lassen sich insgesamt sechs Optionen ein- und ausschalten. Weist man jeder Option eine Stelle einer Binärzahl zu, so lässt sich jede Kombination als Wert darstellen:
1 0 1 0 1 1
Jeder Option wird ein Bit zugeordnet.
Natürlich müssen dann noch Konstanten mit »glatten« Binärzahlen definiert werden, die die jeweilige Stelle identifizieren. Folgende Konstanten existieren für die Autosizingmask: NSViewMinXMargin = NSViewWidthSizable NSViewMaxXMargin = NSViewMinYMargin = …
1; = 2; 4; 8;
// // // //
000001 000010 000100 001000
859
SmartBooks
Objective-C und Cocoa – Band 2
Der Witz an der Sache ist es, dass man diese einzelnen Bits separat kombinieren und abfragen kann. Aus den vorangegangenen Ausführungen mag man auf den Gedanken kommen, dass die Kombination über eine Addition läuft. Dies ist aber nicht sauber, da es hier nicht um den Wert der einzelnen Komponenten geht. In C existieren spezielle Bitoperatoren, die Bits kombinieren können. Zu nennen sei hier Bit-Oder mit dem Zeichen |, Bit-Und mit dem Zeichen &, Bit-Exklusiv-Oder mit dem Zeichen ^ und Bit-Komplement mit dem Zeichen ~. Oder Bei der Oder-Operation verhält es sich so, dass das Ergebnis an einer Bitstelle genau dann 1 ist, wenn dieselbe Bitstelle bei einer der beiden Operanden 1 ist: NSViewMinXMargin NSViewWidthSizable NSViewMaxXMargin NSViewMinYMargin NSViewHeightSizable NSViewMaxYMargin Bit-Oder
0 0 0 0 0 1 1
0 0 0 0 1 0 0
0 0 0 1 0 0 1
0 0 1 0 0 0 0
0 1 0 0 0 0 1
1 0 0 0 0 0 1
Ist ein Bit in einer Zeile gesetzt, so ist es im Ergebnis gesetzt.
Im Code sieht eine Kombination von Optionen daher etwa so aus: int mask = NSViewWidthSizable | NSViewHeightSizable;
Und Bei einem Bit-Und wird die entsprechende Stelle des Ergebnisses 1, wenn beide Operanden an dieser Stelle 1 sind. Die Abfrage einer einzelnen Option kann dann mit einer Und-Operation erfolgen: Bitmaske 1 0 1 0 1 1 NSViewWidthSizable 0 0 0 0 1 0 Bit-Und 0 0 0 0 1 0
Durch eine Und-Operation lässt sich eine Option herausfiltern.
if( mask & NSViewWidthSizable ) { // Option ist gesetzt }
860
Kapitel 10
Objective-C-Referenz
Exclusiv-Oder Der Exclusiv-Oder-Operator ^ lässt eine Bitstelle im Ergebnis dann 1 werden, wenn genau eine Stelle der Operanden 1 ist. Sind also beide Stellen 1, so ist das Ergebnis an der entsprechenden Stelle 0. Sprachlich entspricht dies einem Entweder-Oder. Anders interpretiert lässt sich sagen, dass eine Stelle des ersten Operanden in das Ergebnis übernommen wird, wenn der zweite Operand an dieser Stelle eine 0 aufweist, hingegen invertiert wird, wenn der zweite Operand an dieser Stelle eine 1 aufweist. Autoresizing-Mask 0 1 0 0 1 1 NSViewWidthSizable 0 0 1 0 1 0 Bit-Exklusiv-Oder 0 1 1 0 0 1
Exklusiv-Oder ermöglicht gezieltes Invertieren.
Nicht-Operator Der Nicht-Operator ~ dreht das Ergebnis an jeder Stelle um. Dieser Operand arbeitet nur auf einem Operanden, nämlich dem nachfolgenden. Schiebeoperatoren Mit den Schiebeoperatoren > lassen sich die Bits einer Ganzzahl verschieben: value (Minus-Größer) zur Dereferenzierung von Strukturzeigern und [] (eckige Klammer auf und zu) zur Dereferenzierung von Arrays. Ähnlich verhält sich der Komponentenoperator . (Punkt), der auf eine Komponente einer Struktur zugreift. Man kann auch mit Zeigern rechnen, etwa einen Wert hinzuaddieren. Die Notwendigkeit, dies zu tun, ist in Objective-C allerdings mehr gegeben, da entsprechende Funktionalität durch die Collection-Klassen bereitgestellt wird. Adressoperator Die Adresse einer Variablen wird mit & ermittelt und kann dann einem Zeiger zugewiesen werden. Literale Konstanten haben keine Adresse, wohl aber konstante Variablen (const). NSInteger var; NSInteger* pointer; pointer = &var;
Dereferenzierungsoperator Der Dereferenzierungsoperator * ist das Gegenteil des Adressoperators: pointer = &var; *pointer = 9811; // Zuweisung an var
Komponentenoperator Nicht wirklich ein Dereferenzierungsoperator ist der Komponentenoperator. Er wird zum Zugriff auf die Komponente einer Struktur verwendet: NSRange range = … range.length = 98;
Strukturdereferenzierungsoperator Handelt es sich bei einer Variablen um einen Zeiger auf eine Struktur, so können der Dereferenzierungsoperator * und der Komponentenoperator . kombiniert werden: NSRange range = … NSRange pointerToRange = ⦥ // Zeiger auf Struktur
862
Kapitel 10
Objective-C-Referenz
// Gleichwertig: (*pointerToRange).length = 98; pointerToRange->length = 98;
Arrayoperator Bei einem Zugriff auf Felder wird der Arrayoperator [] verwendet, der nichts mit Nachrichten zu tun hat. int array[98]; array[11] = 5;
Aufzählungsoperator Es können mehrere Ausdrücke mit , (Komma) aufgezählt werden. Dabei wird garantiert, dass die Ausdrücke in der angegebenen Reihenfolge ausgeführt werden. Das Ergebnis des Ausdruckes entspricht dem letzten Element der Aufzählung: a++, a*a // (a+1)*(a+1)
Wenn man anders als hier vorgeschlagen isolierte realease-Nachrichten etwa im -dealloc akzeptiert, so ist folgende Formulierung üblich: [instance release], instance = nil;
Größenoperator Der Größenoperator sizeof() liefert den Speicherbedarf eines Gebildes. Es handelt sich tatsächlich um einen Operator, nicht um eine Funktion. Als Operand werden nämlich nicht nur literale Konstanten und Variablen akzeptiert, sondern auch Typen. Das Ergebnis hat den Typen size_t: NSInteger var = …; size_t size; size = sizeof( var ); size = sizeof( NSInteger );
Bedingter Ausdruck C kennt einen Operator ?: mit drei Operanden, der gerne auch – fälschlich – als »bedingte Zuweisung« bezeichnet wird. Als Ergebnis des Operators wird der zweite Operand geliefert, wenn der erste Operand ungleich 0 ist, ansonsten der dritte Operand. Bedingung?Wahr-Ergebnis:Falsch-Ergebnis
863
SmartBooks
Objective-C und Cocoa – Band 2
Dies kann etwa für die Betragsfunktion verwendet werden oder für Maxima: (a>0)?a:-a // Betrag von a (a>10)?10:a // a, aber höchstens 10
Er ist sinnvoll, wenn ansonsten ein if nur Zuweisungen enthielte. Auch wenn aufgrund der Prioritätsregeln meist nicht erforderlich, sollten die drei Operanden zur Lesbarkeit geklammert werden, wenn sie aus mehr als einem einfachen Ausdruck bestehen.
Zuweisungsoperatoren In Objective-C sind Zuweisungen Ausdrücke, = also ein Operator. Der Ausdruck hat das Ergebnis des zugewiesenen Wertes: a = (b = 5) + 2; // b wird 5 und a wird 7
Zurückhaltend eingesetzt vereinfacht dies manche Schreibweisen. Dies kann allerdings auch auf Kosten der Lesbarkeit erfolgen. Anerkannt sinnvoll sind Fälle, in denen der zugewiesene Wert gleich geprüft wird: while( (item = [enumerator nextObject]) ) if( (self = [super init]) )
Der Zuweisungsoperator kann mit anderen Operatoren kombiniert werden, wobei er links gesetzt werden sollte: a = a + 5 a += 5
Man sollte sich auf eindeutige Fälle beschränken, die inhaltlich an der Operation (»a um 5 erhöhen«) orientiert sind und nicht an der möglichst kurzen Schreibweise. Prinzipiell sind daher auch Ausdrücke wie folgender möglich, allerdings eher verwirrend denn erklärend: b = (a += 5) + 2 // a um 5 erhöhen b auf (neues) a + 2 setzen
lvalue Ausdrücke liefern Werte. Bei den Zuweisungsoperatoren und auch bei dem Inkrement- bzw. Dekrementoperator war es jedoch zusätzlich so, dass ein Wert geschrieben wird. Wir hatten als Ziel dieser Schreibwirkung bisher Variablen verwendet. Man kann dies allerdings allgemeiner auffassen, wenn man sich folgende Zuweisungen ansieht: 864
Kapitel 10
Objective-C-Referenz
pointer->width = 5.0 *(pointer+5) = 'a'; array[3+2] = 4
In beiden Fällen ist das Ziel der Zuweisung nicht einfach eine Variable, sondern wieder ein Ausdruck. Man nennt diesen lvalue (left value). Es gibt gerade in Zusammenhang mit Pointerarithmetik recht komplexe Fälle, die man mangels Pointerarithmetik in Objective-C kaum benötigt. Letztlich muss ein Ausdruck auf der linken Seite aber auf eine Variable oder Speicherstelle evaluieren, sei es über einen Bezeichner, sei es über einen Zeiger.
Ausführungsoperator Da der Name einer Funktion einen Funktionszeiger bildet, kann der Aufruf als Operation auf den Zeiger augefasst werden. Die Klammerung mit () wäre dann der Ausführungsoperator. Entsprechendes gilt bei analoger Betrachtung für Nachrichten für die eckigen Klammern.
Klammeroperator, Priorität Wie auch bei dem Merksatz »Punkt- vor Strichrechnung« existieren Regeln für die Ausführung. Diese Priorität kann durch Klammerung abgeändert werden: 5 * 3 + 2 // 5 * 3 berechnen und 2 addieren (5 * 3) + 2 // Ebenso explizit 5 * (3 + 2) // 3 +2 berechnen und mit 5 multiplizieren
Ohne Angabe durch den Klammeroperator gelten die folgenden Prioritäten: Priorität Operator Beschreibung 15
()
Priorität (Ausdruck)
15
[]
Arraydereferenzierung (Array, Ganzzahl)
15
->
Strukturdereferenzierung (Strukturzeiger, Komponente)
15
.
Komponentenreferenzierung (Struktur, Komponente)
14
!
logisches Nicht (Bedingung)
14
~
Bitkomplement (Ganzzahliger Ausdruck)
14
++ --
Erhöhung (lvalue), Verminderung (lvalue)
14
+-
positives Vorzeichen (Ausdruck), negatives Vorzeichen (Ausdruck)
865
SmartBooks
Objective-C und Cocoa – Band 2
Priorität Operator Beschreibung 14
()
Typcasting (Typ)
14
*
Dereferenzierung (Zeiger)
14
&
Adresse (Variable)
14
Sizeof
Speicherbedarf (Variable oder Typ)
13
*/
Multiplikation, Division (Ausdruck, Ausdruck)
13
%
Modulo (ganzzahliger Ausdruck, ganzzahliger Ausdruck)
12
+-
Addition (Ausdruck, Ausdruck), Subtraktion (Ausdruck, Ausdruck)
11
>
Bitshift (ganzzahliger Ausdruck, ganzzahliger Ausdruck)
10
=
Bitshift und Zuweisung (Ganzzahlvariable, ganzzahliger Ausdruck)
9
!= ==
Gleichheit (Ausdruck, Ausdruck)
8
&
Bit-Und (ganzzahliger Ausdruck, ganzzahliger Ausdruck)
7
^
Bitkomplement (ganzzahliger Ausdruck)
6
|
Bit-Oder (ganzzahliger Ausdruck)
5
&&
logisches Und (Ausdruck)
4
||
logisches Oder (Ausdruck)
3
?:
Bedingte »Zuweisung« (Ausdruck, Ausdruck, Ausdruck)
2
op= =
Zuweisung (lvalue, Ausdruck)
1
,
Aufzählung (Ausdruck, Ausdruck)
Klassen Durch die objektorientierte Erweiterung kennt Objective-C Klassen und Zeiger auf »Instanzen dieses Typs« (Instanzen). Klassen werden in der Source definiert und sind danach im Wesentlichen nicht mehr veränderbar. Instanzen werden zur Laufzeit vom Programm erzeugt. Wenn man also so will, sind Klassen statisch und bei Übersetzung bekannt, während Instanzen dynamisch und bei Übersetzung noch unbekannt sind. Die Verbindung zwischen Instanz und ihrer Klasse ist in Objective-C übrigens wegen des Schlüsselwortes id bei Übersetzung dem Compiler unbekannt und wird daher erst zur Laufzeit aufgelöst (Dynamic-Typing). Eine Klasse wird in zwei Schritten erzeugt, der (Interface-)Deklaration und der (Implementierungs-)Definition. Die Deklaration ist lediglich ein äußeres Verspre-
866
Kapitel 10
Objective-C-Referenz
chen, welches von der Definition eingehalten werden muss. Die Definition kann aber umfassender als die Deklaration sein.
Deklaration Die Deklaration hat im Wesentlichen fünf Teile:
• • • • •
die Benennung der Klasse und der zugehörigen Basisklasse; eine Liste von Protokollen, deren Implementierung versprochen wird; eine Liste der Instanzvariablen; eine Liste von (öffentlichen) Propertys; eine Liste von (öffentlichen) Methoden.
Die Deklaration einer Klasse wird mit @interface eingeleitet und mit @end beendet. Dazwischen befindet sich in geschweiften Klammern optional die Liste der Instanzvariablen und optional eine Liste von Eigenschaften und Methoden. @interface Klassenname : Basisklassenname< Protokollliste > { Liste der Instanzvariablen } Liste der Eigenschaften und Methoden @end
Klassenangabe Mit @interface wird die Beschreibung der Klasse an sich eingeleitet: Klassenname Der Name der Klasse wird durch einen C-Bezeichner gebildet. Nach den Namensregeln beginnt er mit einem Großbuchstaben und wird im Camel-Case fortgeführt. Camel-Case bedeutet hierbei, dass zusammengesetzte Namen ohne Leerzeichen zusammengeschrieben werden und der erste Buchstabe eines Wortes nach dem ausgelassenen Leerzeichen groß geschrieben wird: EinZusammengesetzterBezeichner. Tricks wie ein vorangestelltes »C« sind nicht zu verwenden.
867
SmartBooks
Objective-C und Cocoa – Band 2
BEISPIEL Dies hängt also davon ab, ob man in menschlicher Sprache ein Leerzeichen setzen würde oder nicht. Nur ist das auch nicht eindeutig, weshalb sich etwa in Cocoa sowohl die Schreibung …filename (für die englische Schreibung »filename«) wie auch fileName (für die englische Schreibung »file name«) findet. Basisklasse Die Basisklasse gibt den Ausgang der Ableitung an. Es reicht nicht aus, die Klasse mittels @class als Forward-Declaration zu deklarieren. Vielmehr muss das Interface bekannt sein, weshalb zumeist ein Import erforderlich ist. Alle Cocoa-Klassen sind bereits durch den Import von Cocoa.h bekannt. Es ist möglich, auf die Basisklasse zu verzichten und so einen neuen Klassenstamm zu eröffnen. Hiervon ist allerdings abzuraten, da sogar fundamentale Dinge wie Allokation und Initialisierung in NSObject enthalten sind. Anders als in anderen Programmiersprachen ist das ja keine Frage der Sprache selbst. Protokollliste Die Liste der Protokolle folgt dem Basisklassennamen in spitzen Klammern. Mehrere Protokolle können durch Kommata getrennt werden. Es ist zulässig, dass sich die Methoden in den Protokollen überschneiden. Es reicht nicht aus, dass die entsprechenden Methoden in einer Kategorie implementiert werden.
Instanzvariablen Die Deklaration einer Instanzvariablen folgt den allgemeinen Regeln. Es kann das Makro IBOutlet vorangestellt werden, um dem Interface Builder anzuzeigen, dass er diese Instanzvariable zur Bearbeitung anzeigen soll. Eine Funktion während des Programmlaufes ergibt sich hieraus nicht. Instanzvariablen werden mit einem Kleinbuchstaben begonnen und folgen dann dem Camel-Case. Apple hat einen beginnenden Unterzug für sich reserviert, was Probleme bereiten kann (Band I, S. 180). Qualifier Der Qualifizierer const ist nicht sinnvoll, da in der Instanzvariablenliste selbst keine (initiale) Zuweisung erfolgen darf. static ist ebenso unzulässig wie extern. Sichtbarkeit Instanzvariablen können mit einer Sichtbarkeit versehen werden. Es darf mehrfach zwischen den Optionen hin und her gewechselt werden. 868
Kapitel 10
Objective-C-Referenz
{ Instanzvariablen mit Default-Scope @option Liste der Instanzvariablen mit angegebenen Scope @option Liste der Instanzvariablen mit angegebenen Scope … }
Diese haben folgende Bedeutung: Option
Funktion
@private
Die Instanzvariable ist nur innerhalb der Klassendefinition sichtbar.
@protected Die Instanzvariable ist in der Klassendefinition und in der Definition von Subklassen sichtbar. @public
Die Instanzvariable ist auch außerhalb der Klassendefinition sichtbar.
@package
Die Instanzvariable ist innerhalb der Build-Einheit public, ansonsten privat.
Fremd -method
Klasse private protected public package
Subklasse -method
Package
Sichtbarkeit innerhalb (oben) und außerhalb (unten) eines Frameworks.
Ist der Zugriff innerhalb einer Klassendefinition erlaubt, so gilt dies auch für Kategorien der Klasse. Die letzte Option dient zum Erstellen von Frameworks, bei denen man nur innerhalb der Sourcen dieses Frameworks auf die Variable zugreifen können will. Nicht möglich ist der Zugriff von Appliaktionen, die das Framework benutzen und die Klasse aus dem Framework ableiten. Wird die Option angegeben, so gilt sie für alle darauf folgenden Instanzvariablen. Bis zur ersten Angabe gilt @protected als Default. Da dies in aller Regel die gewünschte Einstellung ist, braucht keine Angabe zu erfolgen. 869
SmartBooks
Objective-C und Cocoa – Band 2
POWER Allerdings erscheint es uns noch sicherer, @private zu verwenden. Subklassen sollten ohnehin Accessoren verwenden, um die Gefahr des Fragile-Base class-Problems zu vermindern. Der Zugriff erfolgt – wenn zulässig – innerhalb der Klasse durch Angabe des Namens ohne einen Zusatz wie self. Außerhalb, was nur bei @public oder @package innerhalb des Frameworks möglich ist, wie bei einer Struktur, also mit -> oder . bei entsprechender Klammerung.
Methoden Nach der geschlossenen Klammer erfolgt die Angabe der Eigenschaften und Methoden, wobei in beliebiger Reihenfolge gemischt werden darf. In Wahrheit handelt es sich bei Eigenschaften um verkappte Methoden. Diejenigen Methoden, die öffentlich bekannt sein sollen, werden im Interface deklariert. Der Bezeichner einer Methode wird anfänglich klein geschrieben und folgt dann dem Camel-Case, wobei jeder neue Parameter wiederum klein geschrieben wird. Die Methodendeklaration wird mit einem Semikolon abgeschlossen. ± (Rückgabetyp)verbMitParameter:(Parametertyp)parameter undParameter2:(Parametertyp)parameter2; Klassenmethoden und Instanzmethoden Zunächst erfolgt mittels Minus- und Pluszeichen, ob eine Instanz- bzw. Klassenmethode gewünscht ist. Hieraus ergibt sich letztlich, ob zulässiger Adressat einer Nachricht das Instanz- oder Klassenobjekt ist. Es ist zulässig, gleichnamige Instanzund Klassenmethoden zu haben. Rückgabetyp Hiernach erfolgt in runden Klammern die Angabe des Typs eines etwaigen Rückgabewertes. Soll keine Rückgabe erfolgen, so ist void anzugeben. Bezeichner Soll die Methode keinen Parameter nehmen, so ist nur noch der Bezeichner zu schreiben, der tunlichst mit einem Verb beginnt. Handelt es sich um einen Accessor, so ist der Name der Eigenschaft zu verwenden. Die Methodendeklaration ist sodann mit einem Semikolon abzuschließen. Sollte die Methode Parameter nehmen, so gehören alle Parameterbeschreibungen ebenfalls zum Methodenbezeichner. Nicht Bestandteil sind indessen die Typen des 870
Kapitel 10
Objective-C-Referenz
Parameters, so dass Überladen in Objective-C nicht möglich ist. Dies erübrigt sich ja auch, da Parameter beschrieben werden. Parameter Wird ein Parameter genommen, so ist seine Beschreibung unmittelbar, also ohne Leerzeichen und vor dem Semikolon dem bisherigen Methodenbezeichner anzuhängen. Er ist mit einem Doppelpunkt zu kennzeichnen. Hiernach folgt in runden Klammern der Parametertyp. Es ist zulässig, jedoch nicht schön, diese Angabe einschließlich der runden Klammern wegzulassen. Dann wird id angenommen. Als Nächstes folgt ein Name für die Parametervariable. Der Name ist anzugeben, obwohl in der Methodendefinition davon abgewichen werden kann. Weitere Parameter Weitere Paramater sind nach einem Leerzeichen (oder entsprechendem) zu beschreiben und wiederum unmittelbar mit einem Doppelpunkt abzuschließen, dem wiederum in runden Klammern der Typ und sodann ein Name für die Parametervariable folgt.
Eigenschaften Die Deklarationen von Eigenschaften beziehen sich auf Accessoren und stellen daher Methodendeklarationen und nicht Deklarationen von Instanzvariablen dar. @property( Optionsliste ) Typ name;
Wird keine Option genutzt, so können die runden Klammern weggelassen werden. Leere Klammern sind ebenfalls zulässig. Zugriffsoptionen Es kann bestimmt werden, ob auf eine Eigenschaft nur lesend oder lesend und schreibend zugegriffen werden kann. Hierzu dienen die Optionen readonly und readwrite. readonly bedeutet, dass lediglich ein Getter versprochen wird. Ob intern ein Setter vorhanden ist, wird damit nicht gesagt. Es geht um die Semantik der Eigenschaft, nicht um ihre Implementierung. readonly wird man also dann verwenden, wenn man nicht möchte, dass diese Eigenschaft von außen gesetzt wird: Sei es, weil es sich um eine berechnete Eigenschaft handelt, die nicht gesetzt werden kann, sei es, weil sie nur intern gesetzt werden soll, meist in Abhängigkeit von anderen Eigenschaften.
871
SmartBooks
Objective-C und Cocoa – Band 2
Aus diesem Grunde ist es möglich, in Class-Continuations und Subklassen von readonly auf readwrite zu wechseln. Besonders im ersten Falle ist das praktisch relevant. So können Eigenschaften extern als read-only markiert und intern mittels @synthesize mit Setter erzeugt werden. Default ist readwrite. Es muss daher nicht angegeben werden, wenn eine neue Eigenschaft als Schreib-Lese-Eigenschaft angelegt wird. – Und es sollte nicht angegeben werden: Denn die Fälle des letzten Absatzes lassen sich anzeigen, wenn man readwrite auf diese Fälle beschränkt. Verwenden Sie also readwrite nur dann, wenn Sie von readonly wechseln. Setter-Speicherverwaltungsoptionen und Kapselung Es existieren drei Optionen zur Steuerung der Speicherverwaltung, von denen jeweils nur eine verwendet werden darf. Sie beziehen sich auf den Setter. Dennoch ist die Angabe auch bei Verwendung der Option readonly zulässig und sinnvoll, da in einer Subklasse auf readwrite gewechselt werden könnte. Schlüsselwort Funktion assign
Der neue Wert wird einfach zugewiesen.
retain
Der Referenzzähler der Instanz wird erhöht und dann der Wert zugewiesen (keine Kapselung).
copy
Es wird eine Kopie der Instanz erzeugt und diese zugewiesen (Kapselung).
Bei einfachen Typen ist nur die Angabe assign zulässig und sinnvoll. Die Zuweisung eines C-Typen verhält sich so wie das Erstellen einer Kopie. Bedenken Sie aber bitte die Zuweisungsregeln bei C-Feldern. Die etwas kompliziert klingende Beschreibung für Instanzen findet seine Ursache darin, dass es sich um eine semantische Einteilung handelt. Ob tatsächlich eine retain- bzw. copy-Nachricht geschickt wird, ist durch die Angabe in der Property-Deklaration noch nicht gesagt. Es muss sich für den Nutzer der Klasse nur so »anfühlen«. Dies bedeutet, dass bei assign und bei retain eine Änderung am übergebenen Wert zu einer Änderung des gespeicherten Wertes führen muss, während dies bei copy gerade nicht der Fall ist. Bei retain muss die Benutzung durch eine Erhöhung des Referenzzählers angezeigt werden, während dies bei assign nicht der Fall sein darf. (Bei copy erübrigt sich die Frage.) Es war übrigens genau das Ziel der Angabe, dies im Header publik zu machen und so die Semantik der Accessoren zu publizieren.
872
Kapitel 10
Objective-C-Referenz
Default ist assign. Ist der Typ der Eigenschaft eine Instanz, so führt die implizite (Nicht-)Angabe zu einer Warnung. Daher ist bei Instanzen stets eine Option aus dieser Kategorie anzugeben, auch dann, wenn der Default assign verwendet wird. Wir halten es für eine gute Regel, umgekehrt bei C-Typen auf assign zu verzichten, um den Charakter kundzutun. Accessoroptionen Mit getter=getterMethode
und setter=setterMethode:
kann explizit angegeben werden, dass andere Accessormethoden verwendet werden sollen. Dies funktioniert auch in Zusammenhang mit @synthesize, so dass entsprechende Methoden erzeugt werden und nicht Accessoren, die auf den Namen der Eigenschaft hören. Wer für den boolschen Getter ein is als Präfix verwenden möchte, kann dies mit der getter-Option erreichen. Unabhängig davon, ob die Accessoren synthetisiert wurden, folgt hieraus eine Abweichung von der Regel, dass sich Dot-Notation und explizite Accessoren synonym verhalten: @property( getter=attrib, setter=setAttrib: ) NSString* attribute; … instance.attribute = @"Amin"; [instance setAttrib:@"Amin"]; // Aequivalent [instance setAttribute:@"Amin"]; // Fehler
Im Default heißen die Accessoren wie die Eigenschaft. Locking und Getter-Speicherverwaltung Die automatisch erzeugten Accessoren führen ein Lock durch, so dass sich die gesetzte bzw. abgefragte Eigenschaft während eben dieses Vorganges nicht ändern kann. Dies erscheint uns von untergeordneter Bedeutung, da die unterbrechungsfreie Durchführung der Accessoren nicht dazu führt, dass die gespeicherte Eigenschaft noch gültig ist. Für die Richtigkeit im Sinne des Threading ist ohnehin ein eigener Lock notwendig.
873
SmartBooks
Objective-C und Cocoa – Band 2
Dennoch ergibt sich hieraus ein zweiter wichtiger Punkt, der mittelbar auch mit einem Problem außerhalb des Threadings zusammenhängt: Der synthetisierte Getter für eine Instanz liefert nicht nur den Wert der Instanzvariable zurück, sondern schickt vorher an ihn noch eine retain-Nachricht sowie eine autorelease-Nachricht. Dies bedeutet, dass die zurückgelieferte Instanz im Autorelease-Pool des aktuellen Threads gesichert ist und fortlebt, auch wenn ein anderer Thread die Eigenschaft verändert. Auch dies wird häufig durch Locking im Anwendungsprogramm explizit gesichert sein. Allerdings spielt es auch eine Rolle, wenn man kein Threading verwendet: Bei einer Eigenschaft, die nur noch von einer Instanz referenziert wird, würde nämlich eine lokale Variable, die auf diese Eigenschaft verweist, ungültig, wenn das referenzierende Objekt gelöscht wird. Durch diese zusätzlichen Nachrichten wird dieses Problem gelöst. Sie können den Lock beim Setter und die zusätzlichen Nachrichten beim Getter dadurch verhindern, dass Sie die Option nonatomic angeben. Dies führt zu Performancevorteilen.
Forward-Declaration Mit dem Schlüsselwort @class können Klassenbezeichner vorwärts deklariert werden. @class Klassenname;
Dies reicht zur Verwendung in Typisierungen aus, nicht jedoch zur Ableitung. Im letzten Falle muss die Basisklasse importiert werden. Dies sollte aber auch umgekehrt nur in diesem Falle erfolgen. Wird die fremde Klasse lediglich für die Typisierung von Instanzvariablen und Methoden benötigt, so ist die Vorwärtsdeklaration zu verwenden und auf ein Import zu verzichten.
Class-Continuation Mit Class-Continuation, Extensions oder anonymen Kategorien bezeichnet man eine mit dem 64-Bit-Runtime-Environment mögliche Fortführung der Klassendeklaration. Es handelt sich von der Syntax her um Kategorien, denen ein Name fehlt. Typischerweise finden sie sich in der Implementierungsdatei vor der Implementierung: @interface Klasse() Liste der Methoden und Eigenschaften @end
874
Kapitel 10
Objective-C-Referenz
Eine solche Kategorie kann wie jede andere die Liste der Methoden und Eigenschaften erweitern. Dies gewinnt insbesondere dann Bedeutung, wenn in einer Class-Continuation eine neue Eigenschaft deklariert wird, für die keine Instanzvariable existiert. Es ist dann möglich, mit @synthesize auch diese herstellen zu lassen. Das führt in der Sache dazu, dass man private Eigenschaften deklarieren und definieren kann. Außerdem kann durch einen Wechsel von readonly nach readwrite dafür gesorgt werden, dass Eigenschaften für Nutzer der Klasse nur lesbar sind, ein in der Implementierung sich anschließendes @synthesize aber auch den Setter erzeugt. Der Preis hierfür ist es, dass alle in Class-Continuation angegebenen Methoden einschließlich Eigenschaften nur in der (Haupt-)Implementierung der Klasse definiert werden dürfen. Es existiert also keine gesonderte Implementierung von Class-Continuations.
Implementierung Die Implementierung enthält keine Instanzvariablen, sondern nur die Umsetzung der versprochenen Methoden.
Methodendefinition Der Kopf der Methodendefinition folgt den Regeln der Methodendeklaration. Es ist sogar erlaubt, dass auch die Methodendefinition ein Semikolon nach dem Kopf enthält. Der Unterschied liegt alleine darin, dass nach dem Methodenkopf ein mit geschweiften Klammern eingeschlossener Block von Anweisungen folgt. Die Namen der Parametervariablen müssen nicht denen der Methodendeklaration entsprechen.
Eigenschaftsdefinition Die von der Eigenschaft versprochenen Accessoren, bei Read-Only-Eigenschaften lediglich die Getter, können wie normale Methoden ausprogrammiert werden. Es ist jedoch auch möglich, sich die entsprechenden Methoden automatisch erzeugen zu lassen. Hierzu dient @synthesize. Als Ergebnis erhält man Implementierungen, die der Semantik der Property-Deklaration entsprechen. Lediglich in Bezug auf die – ja nicht-öffentlichen – Instanzvariablen ist die Synthetisierung daher noch zu parametriesieren. 875
SmartBooks
Objective-C und Cocoa – Band 2
Implizite Instanzvariablen Existiert eine zur Eigenschaft gleichnamige Instanzvariable, so benutzt die Synthetisierung automatisch diese zur Ablage der Daten. Dies liegt auf der Hand. Explizite Instanzvariablen Es ist jedoch auch möglich, von dieser Projektion abzuweichen und eine andere Instanzvariable zur Nutzung durch die Accessoren anzugeben: @interface … { Typ instanzvariable; } @property( … ) Typ eigenschaft; … @synthesize eigenschaft = instanzvariable;
Dies ist praktisch, wenn aufgrund von bereits belegten Bezeichnern ein anderer verwendet werden soll oder bei Fortführungen unter einem anderen Namen. Automatische Instanzvariablen Das 64-Bit-Runtime lässt es zu, dass auch die Instanzvariablen mit den Accessoren synthetisiert werden. Existiert keine Instanzvariable des gleichen bzw. angegebenen Namens, so wird automatisch eine erzeugt. Es ist übrigens vom Gedanken des Information-Hiding her positiv, dass die Instanzvariablen im Header nicht sichtbar sind. Dynamische Implementierung Mit dem Schlüsselwort @dynamic lässt sich zudem festlegen, dass der Compiler von der Existenz der Methode zur Laufzeit ausgehen soll. Hierdurch wird also ein Warning verhindert, wenn weder ein @synthesize benutzt wird noch manuell ausformulierte Accessoren existieren. Derlei dynamische Methoden lassen sich zur Laufzeit bei einem gescheiterten Message-Dispatch implementieren. Der MessageDispatch wird später besprochen. @dynamic lässt sich auch verwenden, wenn die Accessoren manuell implementiert werden. Da hier jedoch der Compiler – wie auch der Leser der Sourcen – diese sieht, ist es überflüssig. Wir schlagen daher vor, es wirklich nur für dynamische Implementierungen zu verwenden, um so an den Leser der Source einen Hinweis zu geben.
876
Kapitel 10
Objective-C-Referenz
Kategorien Kategorien sind nachträgliche Erweiterungen auch von bereits nur im Compilat vorliegenden Klassen. Sie werden wie eine Klasse durch ein Interface und eine Implementierung umgesetzt. @interface Klassenname( Kategorienname ) Liste der Methoden und Eigenschaften @end … @implementation Klassenname( Kategorienname ) Liste der Methoden und Eigenschaften }
Um Warnungen zu vermeiden, muss der Compiler sowohl die Methode im Interface sehen als auch in der Implementierung. Nicht erforderlich ist es indessen, dass die Methode in der Kategorie implementiert wird, in der sie deklariert wurde. Es ist auch möglich, Propertys in Kategorien zu erklären, jedoch nicht, automatische Instanzvariablen erzeugen zu lassen.
GRUNDLAGEN Apple nennt Kategorien auch »informale Protokolle«, wenn lediglich ein Interface der Kategorie existiert. Auf diese Weise lassen sich Methoden über den gesamten Klassenbaum bekannt machen. Diese Technik hat gerade deswegen erhebliche Nachteile und sollte nicht mehr verwendet werden.
Protokolle Ein Protokoll ist eine Ansammlung von Methoden, die zu implementieren sind. Ein Zeiger auf eine Instanz kann so typisiert werden, dass die Anforderungen des Protokolls erfüllt sein müssen. Die Anforderungen sind nur erfüllt, wenn die Klasse der Instanz die Einhaltung des Protokolls verspricht.
Definition Ein Protokoll wird mit dem Schlüsselwort @protocol eingeleitet. Hierauf folgen die zu implementierenden Methoden, wobei danach unterschieden werden kann, ob 877
SmartBooks
Objective-C und Cocoa – Band 2
eine Methode implementiert werden muss oder lediglich implementiert werden kann. @protocol Liste von @Option Liste von @Option Liste von … }
Protokollname< Enthaltene Protokolle > Methoden und Eigenschaften Methoden und Eigenschaften zur Option Methoden und Eigenschaften zur Option
Protokollangabe Im Kopf erfolgt zunächst die Angabe eines Namens. Es handelt sich um einen CBezeichner, der mit einem Großbuchstaben beginnt und dann im Camel-Case fortgeführt wird. Wie bei einer Klasse können darüber hinaus in spitzen Klammern weitere Protokolle angegeben werden, die automatisch inkorporiert werden. Auch hier ist es gleichgültig, ob eine Methode doppelt verwendet wird, also in mehreren angegebenen Protokollen oder in dem neuen Protokoll gleichnamig enthalten ist.
Methoden und Propertys Methoden und Eigenschaften werden wie im Interface einer Klasse deklariert. Die Angabe von Propertys ist erst seit OS X 10.6 Snow Leopard möglich. Für OS X 10.5 Leopard müssen die Accessoren explizit als Methoden aufgeführt werden.
Optionen Mit den Optionen @optional und @required wird angegeben, ob die nachfolgenden Methoden von der Klasse, die das Protokoll verspricht, implementiert werden müssen oder lediglich können. Wie bei @private usw. in der Definition von Instanzvariablen wirken die Optionen abschnittsweise, also für alle folgenden Methoden, bis eine neue Angabe erfolgt. Es kann frei gewechselt werden.
Forward-Declaration Protokolle können mittels @protocol Protokollname;
vorwärts deklariert werden. Dies reicht aus, um Protokolle in Typangaben zu verwenden, nicht jedoch für die Verwendung in einem Klasseninterface.
878
Kapitel 10
Objective-C-Referenz
Verwendung als Typ Das Protokoll kann zur Erweiterung einer Typangabe verwendet werden: Klasse< Liste der Protokolle > Hierbei wird der Charakter des Protokolls als Typangabe sichtbar. Die Angabe der Protokolle ist im Prinzip bei jeder Typangabe möglich, also nicht nur bei lokalen Variablen, sondern auch bei Instanzvariablen und Parametern. Grundsätzlich gilt, dass an eine auf diese Weise typisierte Instanz alle Nachrichten geschickt werden dürfen, die entweder in der Klasse oder in einem der Protokolle deklariert wurden. Allerdings gilt dies nicht, wenn anstelle der Klasse einfach id angegeben wird. In diesem Falle dürfen nur noch Nachrichten aus den Protokollen verschickt werden, da es ansonsten zu einer Warnung des Compilers kommt. Dies ist in Bezug auf -respondsToSelector: und -performSelector:… unpraktisch, da diese Methoden gerade bei Protokollen mit optionalen Methoden benötigt werden. Als Auswege bieten sich an: eine Zuweisung an eine Variable mit der Typisierung id oder die Verwendung von NSObject anstelle von id.
Verwendung zur Laufzeit Es existieren Protokollobjekte, die zur Laufzeit abgefragt werden können. Sie haben den Typen Protocol und werden mit @protocol abgefragt. Insbesondere ist es möglich, mit -conformsToProtocol: zu ermitteln, ob eine Instanz zu einer Klasse gehört, die das Protokoll implementiert. Dabei ist es nicht entscheidend, ob in der Klasse tatsächlich alle required-Methoden vorhanden sind, sondern ob die Klasse in ihrem Interface die Einhaltung garantierte. Optionale Methoden sind ob ihres Wesens ohnehin unbeachtlich für die Einhaltung des Protokolls.
Ausführungseinheiten AUFGEPASST In C existieren seit jeher Ausführungseinheiten, bei denen Anweisungen mit { und } geklammert werden. Diese heißen klassisch »Block«, was sich mit der C-Erweiterung Blocks von Apple sprachlich beißt. In diesem Abschnitt werden die klassischen Blöcke daher »C-Blöcke« genannt, während die Erweiterung von Apple weiterhin Block heißt. In C sind verschiedene Ausführungseinheiten möglich. 879
SmartBooks
Objective-C und Cocoa – Band 2
Anweisung Gewissermaßen das ausführbare Atom eines Programmes ist die Anweisung. Eine Anweisung besteht aus einem Ausdruck nebst Semikolon oder aus einer Kontrollstruktur wie return, for usw., die selbst wieder eine Anweisung enthalten kann Ausdruck; Kontrollstruktur; Kontrollstruktur Anweisung;
C-Blöcke Jede Anweisung kann durch einen C-Block ersetzt werden. Dieser wird mit einem { eröffnet und mit einem } abgeschlossen. Neben einer oder mehrerer Anweisungen kann ein C-Block drüber hinaus Variablendefinitionen enthalten. { Liste von Variablen und Anweisungen }
In dem typischerweise von Objective-C verwendeten Sprachstandard von C dürfen Variablendefinitionen und Anweisungen bunt gemischt werden. Da jede Anweisung durch einen C-Block ersetzt werden kann, können auch Anweisungen in C-Blöcken wieder durch Anweisungen ersetzt werden, so dass C-Blöcke in C-Blöcken entstehen. Grundsätzlich gilt, dass in jedem C-Block alle Bezeichner sichtbar sind, die auch außerhalb sichtbar waren zuzüglich der von ihm selbst definierten Bezeichner.
Kontrollstrukturen Es existieren verschiedene Konstrukte, die den Ablauf des Programmes von Anweisung zu Anweisung verändern. Man bezeichnet diese als Kontrollstrukturen. Manche Kontrollstrukturen (Verzweigung und Schleifen) beziehen sich auf eine folgende Anweisung. Da sich Anweisungen durch Blöcke ersetzen lassen, kann an ihrer Stelle auch eine Anweisungsfolge stehen. Hiervon sollte unbedingt Gebrauch gemacht werden, weil dies verschiedene Probleme mit Lesbarkeit und Wartbarkeit löst. Im Folgenden wird jedoch die Grundstruktur mit einer Anweisung gezeigt.
880
Kapitel 10
Objective-C-Referenz
Bedingte Anweisung Eine Verzweigung ist mit der if-else-Struktur möglich. Dabei wird dem if ein Wert übergeben. Ist dieser ungleich 0 (NULL für Zeiger, nil für Instanzobjektzeiger und Nil für Klassenobjektzeiger), so wird die nach der schließenden Klammer folgende Anweisung ausgeführt, andernfalls die nach dem optionalen else folgende Anweisung: if( Ausdruck ) Anweisung else Anweisung
Die Einrückung ist ohne Wert für den Übersetzungsvorgang und dient nur der Lesbarkeit. Sollen mehrere Bedingungen nacheinander abgearbeitet werden, so existieren ifKaskaden, die zur Verdeutlichung anders geschrieben werden: if( Ausdruck ) Anweisung else if( Ausdruck ) Anweisung else if( Ausdruck ) Anweisung else Anweisung
wird zu if( Ausdruck ) Anweisung else if( Ausdruck ) Anweisung else if( Ausdruck ) Anweisung else Anweisung
Es wird also der folgende Fall immer nur dann geprüft, wenn kein vorangegangener erfüllt war. Die Verschachtelung von if-Zweigen ist unübersichtlich, weil nicht sofort erkennbar ist, worauf sich ein folgendes else bezieht. Es gilt jedoch die Regel, 881
SmartBooks
Objective-C und Cocoa – Band 2
dass sich else immer auf das nächstäußere if bezieht, welches noch kein else besitzt. Richtig gesetzt also: if( Ausdruck ) if( Ausdruck ) Anweisung else Anweisung
Derlei Missverständnisse vermeidet man, indem man bei if-Konstrukten stets die Anweisung durch einen C-Block ersetzt.
Mehrfachauswahl Die Mehrfachauswahl eines Wertes ist mit dem switch-Konstrukt möglich. switch( Ausdruck ) { case Konstante1: Liste von Anweisungen case Konstante2: Liste von Anweisungen default: Liste von Anweisungen }
Das switch-Konstrukt wertet den Ausdruck aus und setzt die Programmfortführung bei demjenigen case-Zweig fort, dessen Konstante dem Wert des Ausdruckes entspricht. Ist kein Treffer dabei, so wird die Ausführung bei einem optionalen default-Zweig fortgeführt. Nach Abarbeitung eines case-Zweiges wird nicht etwa das Konstrukt verlassen, sondern die Ausführung im nächsten Zweig fortgesetzt. Soll dies – wie meist – nicht der Fall sein, so ist das switch-Konstrukt mit einem expliziten break zu verlassen. Auf diese Weise können Anweisungen für verschiedene Ergebnisse des Ausdrucks zusammengefasst werden, indem man die Zweige einfach leer lässt.
while-Wiederholung Die einfachste Wiederholung stellt die Schleife mit while dar. Die ihr folgende Anweisung wird so lange wiederholt, bis der Ausdruck in den Klammern hinter dem while 0 usw. wird: while( Ausdruck ) Anweisung
882
Kapitel 10
Objective-C-Referenz
Dementsprechend muss entweder im Ausdruck selbst oder in der Anweisung etwas so im Ausdruck geändert werden, dass er irgendwann 0 wird. Allerdings kann dies auch ein externer Zustand sein, etwa das Erhalten eines Locks, weil ein anderer Thread in freigegeben hat. Der Ausdruck wird vor jedem Eintritt in den Schleifenkörper geprüft, so dass die Anweisung kein einziges Mal ausgeführt wird, wenn bereits bei Beginn der Ausdruck zu 0 evaluiert (ablehnendes Verhalten).
do-Wiederholung Ganz ähnlich verhält sich die do-Schleife. Sie hat den Aufbau do Anweisung while( Ausdruck );
Es gelten allerdings einige Besonderheiten:
•
Obwohl durch die beiden Schlüsselwörter der Schleifenkörper begrenzt wird, ist nur eine Anweisung dazwischen erlaubt – oder eben ein C-Block.
•
Der Schleifenkörper wird mindestens ein Mal ausgeführt, da die Abfrage der Bedingung am Ende erfolgt (annehmendes Verhalten).
•
Das Konstrukt ist mit einem Semikolon abzuschließen.
for-Wiederholung Die komplexeste Wiederholung stellt die for-Schleife dar. Sie besteht genau genommen aus drei Ausdrücken und einer Anweisung: for( Startausdruck; Vergleichsausdruck; Endausdruck ) Anweisung
Der Startausdruck wird einmalig vor dem Schleifenkörper ausgeführt, der Vergleichsausdruck vor jedem Durchgang und der Endausdruck nach jedem Durchgang. Ersatzweise sieht dies so aus: Startausdruck; while( Vergleichsausdruck ) { Anweisung Endausdruck; }
883
SmartBooks
Objective-C und Cocoa – Band 2
Auch wenn man daraus üble Dinge bauen kann, sollte man sich auf Fälle beschränken, in denen im Startausdruck eine Zählervariable initialisiert, im Vergleichsausdruck eine Abbruchbedingung geprüft und im Endausdruck die Zählervariable verändert wird.
for-in-Wiederholung Die for-in-Schleife wurde von Objective-C 2.0 eingeführt und erlaubt das Iterieren über beliebige Graphen. Das zu iterierende Objekt muss das Protokoll NSFast Enumeration mit der Methode countByEnumeratingWithState:objects:count: implementieren. Es ist möglich, jedoch nicht zwingend erforderlich, die Iterationsvariable erst im Schleifenkopf zu definieren: for( Klasse* item in IterierbaresObjekt ) Anweisung
break break bricht die Ausführung einer Wiederholung oder eines switch-Konstruktes ab. Die weiteren Anweisungen im C-Block werden nicht ausgeführt.
continue Hiermit wird innerhalb einer Wiederholung zum Anfang des Wiederholungskörpers gesprungen. Etwaige Tests werden dort ausgeführt.
Funktionen Funktionen sind so etwas wie benannte C-Blöcke. Sie können jedoch Parameter erhalten. Es ist zwischen der Bekanntgabe einer Funktion (Deklaration) und der Definition zu unterscheiden.
Deklaration und Definition // Funktionsdeklaration Typ funktionsname( Paramterliste ); // Funktionsdefinition Typ funktionsname( Parameterliste ) { Liste von Variablendefinitionen und Anweisungen }
Die Parameterliste enthält eine durch Kommata getrennte Auflistung der Parameter, wobei jeder Eintrag aus dem Typen und einem Variablennamen besteht. Bei der Funktionsdeklaration kann der Variablenname weggelassen werden. 884
Kapitel 10
Objective-C-Referenz
Die Parameter werden grundsätzlich by-Value, also durch Kopieren übergeben. Dies bedeutet, dass eine Veränderung innerhalb der Funktion nicht außerhalb sichtbar ist. Allerdings gilt dies freilich bei Zeigern nur für den Zeiger selbst, nicht für den dahinter stehenden Wert. Dieser kann in der Funktion nach außen merkbar verändert werden.
return Mit return wird die Ausführung einer Funktion abgebrochen. Besitzt die Funktion einen Rückgabewert, kann und muss hinter dem Schlüsselwort dieser angegeben werden: return Ausdruck;
Blocks Die bereits angesprochenen Blocks stellen parametrisierbare C-Blöcke dar. Sie ähneln auf den ersten Blick Funktionen, unterscheiden sich aber dadurch, dass sie keinen Namen besitzen und außerdem ihren Kontext anders behandeln. Es handelt sich inhaltlich um so etwas wie Funktionen, die zusätzlich ihren äußeren Kontext erhalten. Ein Block kann innerhalb eines C-Blocks, etwa einer Funktion, oder außerhalb global definiert werden: ^( Parameterliste ) { Liste von Anweisungen }
Die Dokumentation von Apple nennt dies einen »Block-Literal« analog zu Konstanten. Block-Literale nennen also anders als Blockreferenzen nicht explizit ihren Rückgabewert. Dieser ergibt sich aus den etwaig enthaltenen return-Anweisungen. Findet sich kein return im Block, so ist der Rückgabewert void. Bei mehreren Verwendungen von return müssen diese typmäßig übereinstimmen. Die Parameterliste kann weggelassen werden, wenn sie leer ist. Um einen Block zu benennen, muss wie bei Objekten – Blocks sind übrigens diesen sehr ähnlich – die Blockreferenz benannt werden. NSInteger (^wuerfelGenerator)( void ); wuerfelGenerator = ^{ … }
885
SmartBooks
Objective-C und Cocoa – Band 2
Blocks werden über die Blockreferenz wie eine Funktion ausgeführt. NSInteger wurf = wuerfelGenerator();
Closure Ein Block bildet einen sogenannten Closure. Hiermit bezeichnet man die Verbindung von Code mit seinem Kontext, in dem er definiert wurde. Er merkt sich also seinen Umgebungszustand. Dies wird deutlich, wenn sich die Umgebung verändert: int contextVar; void (^myBlock)( int paraVar ); // Erster Kontext contextVar = 1; myBlock = ^(int paraVar) { NSLog( @"%d %d", contextVar, paraVar ); }; myBlock( contextVar ); contextVar = 2; myBlock( contextVar ); // Zweiter Kontext contextVar = 3; myBlock = ^(int paraVar) { NSLog( @"%d %d", contextVar, paraVar ); }; myBlock( contextVar);
In dem vorstehenden Beispiel wird bei beiden Ausführungen im ersten Kontext für contextVar 1 ausgegeben, da bei der Definition des Blockes contextVar den Wert 1 hatte. Erst wenn der Block erneut definiert wird (zweiter Kontext), ändert sich der Wert, und es wird 3 ausgegeben. Der Block macht also gewissermaßen ein Photo von seiner Umgebung. Die Parameter werden indessen wie üblich übergeben. Für die Parametervariable erscheint daher 1, 2, 3.
Variablen Wie bei Funktionen ist auch bei Blocks der Austausch von Daten mittels Variablen.
886
Kapitel 10
Objective-C-Referenz
Parametervariablen Blocks erhalten Parameter wie Funktionen. Sie werden by-Value übergeben, also durch wertemäßige Kopie. Beachten Sie aber auch hier die Eigenart, dass bei Zeigern nur derselbe kopiert wird, nicht das verwiesene Objekt. Bei jedem Aufruf werden die Parameter neu gesetzt, siehe oben. Lokale Variablen Da Blocks einen C-Block enthalten, können in ihm lokale Variablen deklariert werden. Diese sind nur in ihm sichtbar und gehören zu seinem Kontext. Wird der Block kopiert, so werden sie ebenfalls kopiert. Äußere Variablen Ein Block kennt seine äußeren Variablen wie auch ein innerer C-Block. Dies bedeutet, dass ein globaler Block die globalen Variablen kennt, ein lokaler Block zusätzlich die lokalen usw., wenn Blocks und C-Blöcke ineinander verschachtelt werden. Standardmäßig sind diese äußeren Variablen nur lesbar. Wie aber oben gezeigt, sind auch die äußeren Variablen Bestandteil des Kontextes und daher vom Closure erfasst. Äußere __block-Variablen Wird im äußeren Kontext eine Variable mit __block bezeichnet, so erzeugt der Compiler für jeden benutzenden inneren Block eine Referenz. Dadurch wird die Variable für den inneren Block beschreibbar. Sie wird zwischen allen Kontexten geteilt, also dem äußeren Kontext, in dem der Block definiert wurde, und den Kontexten der verschiedenen Blocks. Die Lebensdauer endet erst mit der Abarbeitung des letzten Kontextes, welchen die Variable benutzt. Daraus ergibt sich jedoch auch, dass die Variable nicht mehr als Snapshot zum Zeitpunkt der Blockerzeugung übernommen wird, sondern dynamisch. Das einleitende Beispiel führt daher dazu, dass sich nunmehr die Änderung von contextVar nach Erzeugung des Blockes im Block auswirkt. Dies ist insoweit bemerkenswert, als lokale Variablen üblicherweise auf dem Stack liegen, der nur einmal vorhanden ist. Der Stack stirbt aber automatisch, wenn die äußere Funktion oder Methode verlassen wird. Daher muss in einem solchen Falle möglicherweise der Stack in den Heap kopiert werden. Die Adresse der Variablen ändert sich also dabei und darf daher nicht zwischengespeichert werden. Objektvariablen und Speicherverwaltung Da Blöcke möglicherweise länger als ihr umgebender Kontext leben und zudem vervielfältigt werden können, müssen von ihm referenzierte Objekte gesichert werden. Deshalb werden zusätzliche retain-Nachrichten geschickt: 887
SmartBooks
Objective-C und Cocoa – Band 2
•
Wird auf eine Instanzvariable mittels ihres Namens (unmittelbar) zugegriffen, so erhält das haltende Objekt, also self, ein retain.
•
Wird anderweitig auf ein Objekt zugegriffen, so erhält das Objekt ein retain.
Lebensdauer Die Lebensdauer eines Blockes hängt wie bei einer Variablen von seinem Scope ab. Das führt dazu, dass in einem inneren Kontext definierte Blöcke mit Verlassen des Scopes möglicherweise zerstört werden. Dann dürfen keine äußeren Variablen darauf zeigen. Dies ist eigentlich nicht neu, da eine Parallelproblematik auch für sonstige dynamische Elemente existiert, die in einem inneren Kontext stecken. Soll ein Block außerhalb des definierten Scopes fortexistieren, so ist er mittels Block_copy() zu kopieren. Technisch wird er damit vom Stack auf den Heap verschoben. Der Stack ist ein Speicherbereich, der mit dem Scope frei wird, etwa wenn eine Methode verlassen wird. Es verhält sich also wie mit lokalen Variablen. Der Heap ist hiervon unberührt. Der Speicher muss dort explizit wieder freigegeben werden. Dies entspricht der Lebensdauer von Instanzen. Die Folge des Block_ copy() ist daher auch, dass der Block mit Block_release() wieder freizugeben ist. Ein Block stellt ein Objekt im Sinne von Objective-C dar. Es ist daher möglich, die Nachrichten retain, release und autorelease an ihn zu schicken.
Nachrichten Es ist wichtig bei Objective-C, zwischen dem Versand von Nachrichten und der Ausführung von Methoden (Aufruf) zu unterscheiden. Nachrichten werden absenderseitig versendet, Methoden empfängerseitig aufgerufen. Dazwischen liegt der Message-Dispatch, der die für die Nachricht passende Methode auswählt. Der Versand einer Nachricht ist in Objective-C zugleich ein Ausdruck, da ein Wert geliefert werden kann. Insofern spricht man auch von »Message-Expression«.
Versand Für den Versand einer Nachricht existieren drei Möglichkeiten unmittelbar in Objective-C und Cocoa:
• •
klassisch durch Verwendung der eckigen Klammern die Verwendung der Methoden -performSelector:… 888
Kapitel 10
Objective-C-Referenz
•
die Methoden -invoke und -invokeWithTarget: der Klasse NSInvocation
Jeder Versand enthält drei Elemente: Adressat, Selektor, gegebenenfalls aktuelle Parameter. Dazu tritt der Rückgabewert als Ergebnis der Nachrichtenabarbeitung, wenn ein solcher vorhanden ist. Adressat Der Adressat einer Nachricht kann jedes Objekt sein, also sowohl Klassen- wie Instanzobjekte. Ein Klassenobjekt kann entweder durch eine entsprechende Variable gegeben sein oder auch aus Bequemlichkeitsgründen unmittelbar durch die Angabe der Klasse als Typ: Class target = NSClassFromString( Klassenname ); [target message]; [Klassenname message];
Ein weiterer zulässiger Adressat ist super. Wie noch ausgeführt wird, ist eine Nachricht an super etwas grundsätzlich anderes als eine Nachricht an id. Selektor Der Selektor ist der Bezeicher der Methode mitsamt der Parameterbeschreibungen. Für - (void)messageWithArgument:(id)argument andArgument:(NSString*)argument2
lautet also der Selektor messageWithArgument:andArgument:. Der Selektor enthält keine Angabe über den Typ des Rückgabewertes und der Parameter. Vereinfacht gesagt wird eine Nachricht durch eine Methode ausgeführt, wenn der Selektor der Nachricht dem Selektor der Methode entspricht. Signatur Die Nachrichten und die Methodensignatur enthält die Typisierung einer Methode ohne weitere Bestandteile wie den Namen. Alle Actionmethoden haben daher denselben Typen void-id-selektor-id, wobei der erste Wert für den Rückgabewert und der zweite und dritte Wert die verborgenen Parameter self bzw. _cmd enthalten. Erst der vierte Eintrag, hier id, beschäftigt sich mit dem ersten sichtbaren aktuellen Parameter. Die Signatur einer Nachricht und einer Methode müssen passen.
889
SmartBooks
Objective-C und Cocoa – Band 2
Aktuelle Parameter Die aktuellen Parameter stellen die in der Nachricht enthaltenen Daten dar. Grundsätzlich sind alle Typen als Parameter zulässig. Wir werden jedoch sehen, dass bei bestimmter Art des Nachrichtenversandes Einschränkungen bestehen können.
Klassisch Die klassische Variante dürfte hinlänglich bekannt sein. Der Selektor wird beim Versand am jeweiligen Parameter aufgebrochen. Auf diese Weise stehen Parameterbeschreibungen und aktuelle Parameter unmittelbar zusammen, was die Lesbarkeit erhöht. An den Bruchstellen ist nach dem aktuellen Parameter ein Leerzeichen einzufügen. (Gebrochener) Selektor und aktueller Parameter werden durch einen Doppelpunkt getrennt. [target message] [target messageWithArgument:argument]; [target messageWithArgument:argument andArgument:argument2];
Perform-Selector-Methoden NSObject implementiert Methoden zu Versand von Nachrichten, die mit -performSelector: beginnen. Der erste Parameter ist hierbei der vollständige, also ungebrochene Selektor. Optionale weitere Parameter bilden die aktuellen Parameter, wobei nur bis zu zwei Parameter erlaubt sind und es sich bei diesen um Objekte handeln muss. Dies gilt auch für den Rückgabewert. Es ist unproblematisch, an void-Nachrichten zu versenden, wenn in diesem Falle der Absender keinen Rückgabewert zuweist.
NSInvocation-Methoden Mittels der Klasse NSInvocation können ebenfalls Nachrichten versendet werden. Durch die Modellierung in eine Klasse können Nachrichten etwa in Instanzvariablen gespeichert werden. Dies ist bei der häufigen Wiederverwendung praktisch. Ausgangspunkt ist die Signatur, welche die Parametrisierung der Nachricht beschreibt. Sie hat den Typen NSMethodSignature. Ein weiterer Unterschied liegt darin, dass NSInvocation auch Nicht-Objekte als aktuelle Parameter behandeln kann, allerdings mit der Ausnahme von Unions. Aus technischen Gründen müssen Objekte als aktuelle Parameter mit einer weiteren Indirektion übergeben werden.
890
Kapitel 10
Objective-C-Referenz
id target = … NSString* argument = …; // [target messageWithArgument:argument] SEL selector = @selector( messageWithArgument: ); NSMessageSignature* signature = [target signatureForSelector:selector]; NSInvocation* message = [NSInvocation invocationWithMethodSignature:signature]; [message setSelector:selector]; [message setArgument:&argument atIndex:2];
Zu beachten ist, dass die Parameter der Nachricht ab dem Index 2 gezählt werden, da am Index 0 der verborgene Parameter self und an Index 1 der verborgene Paramter _cmd steht. -setArgument:atIndex: rechnet dies nicht heraus.
Dispatching Mit Dispatching bezeichnet man die Suche nach einer Methode beim Empfänger, die auf die Nachricht passt. Jede Nachricht wird also »gedispatcht«. (Man hoffe, dass meine Sprachlektorin mich für dieses Wort nicht erschieße! – Dieses Mal nicht). Erst danach wird die Methode ausgeführt. Tatsächlich existieren allerdings in bestimmten Fällen Feinheiten, die es manchmal zu beachten gibt. Dabei geht es weniger um neue Regeln als darum, dass die Anwendung der von Ihnen eigentlich schon verinnerlichten Regeln zu überraschenden Ergebnissen führen kann.
[ anObject doSomething ]
Empfänger init initWithName: anAction: doSomething dealloc
Der Empfänger bestimmt, ob eine Nachricht empfangen werden kann.
Dynamisch gebundene Nachrichten Wird eine Nachricht versendet, so muss eine entsprechende Methode beim Empfänger zur Bearbeitung gefunden werden. Dabei wird grundsätzlich beim Empfänger gesucht, ob er über eine entsprechende Methodenimplementierung verfügt. Dies ist erst einmal unabhängig davon, ob der Empfänger ein Klassen- oder Instanzobjekt ist.
891
SmartBooks
Objective-C und Cocoa – Band 2
Instanzobjekte als Empfänger Nachrichten können an Instanzobjekte versendet werden. Dabei spielt es keine Rolle, ob sie mittels self in einer Instanzmethode, an eine typisierte ID-Variable oder an eine mit id untypisierte ID-Variable versendet werden. - (void)instanceMethod { // Nachricht an typisierte ID-Variable AnyClass* instance = …; [instance message]; // Nachricht an untypisierte ID-Variable id instance = …; [instance message]; // Nachricht an self in Instanzmethode [self message]; }
Da sich in Objective-C Methoden nicht pro Instanz hinzufügen lassen, ergibt sich die verfügbare Methodenliste einer Instanz stets aus der Klasse des Empfängers. [receiver
Subclass*
Superklasse superklasse -method1 …
method2]
receiver
Subklasse isa …
Basisklasse superklasse -method1 -method2 … Subklasse superklasse -method2 -method3 …
Die Methodensuche wird bei der Klasse der Empfängerinstanz gesucht.
Denn die Suche beginnt bei der Klasse der Empfängerinstanz, nicht bei der Klasse, die die ID-Variable hat. Diese ist also irrelevant (Dynamic-Binding). Der Umstand, dass die Typisierung der ID-Variablen unbedeutend ist, erlaubt erst die Typisierung mit id. Es würde daher in der folgenden Abbildung auch -method3 gefunden, obwohl diese in Baseclass gar nicht bekannt ist. Allerdings würde der Compiler eine Warnung ausgeben, da ihm die Sache (zu Recht) spanisch vorkäme. Der Funktionsfähigkeit des Programmes würde dies aber keinen Abbruch tun. 892
Kapitel 10
Objective-C-Referenz
Wird bei der Klasse der Empfängerinstanz die Methode nicht gefunden, so wird in deren Superklasse gesucht usw. Der erste Treffer auf der Suche entscheidet. [receiver
Subklasse*
Superklasse superklasse -method1 …
method1]
receiver
Subklasse isa …
Basisklasse superklasse -method1 -method2 … Subklasse superklasse -method2 -method3 …
Wird bei der Klasse der Empfängerinstanz keine entsprechende Methode gefunden, so wird in der Superklasse weitergesucht.
Klassenobjekte als Empfänger Die Lage bei Klassenobjekten ist nicht anders. Zu solch einer Nachricht kommt es, wenn entweder in einer Klassenmethode eine Nachricht an self geschickt wird oder sich der Adressat einer Nachricht aus einer Klassenvariable ergibt. + (void)classMethod { // Nachricht an Klassenvariable Class aClass = [… class]; [aClass message]; // Nachrich an self in Klassenmethode [self message]; }
Zwei Dinge, die sich aus der Natur der Sache ergeben, sind allerdings zu beachten:
•
Der Zeiger auf ein Klassenobjekt ist stets mit Class typisiert. Er entspricht also in etwa id für Instanzen. (Allerdings darf id auch auf Klassenobjekte verweisen.) Daher erübrigt sich der Hinweis darauf, dass es auf den Typen der IDVariable nicht ankommt.
•
Instanz- wie Klassenmethoden werden bei der Klasse vom Programmierer festgelegt. Handelt es sich bei dem Empfänger einer Nachricht um ein Klassenobjekt, entfällt damit eine Ebene der Indirektion, weil nicht von der Instanz auf die Klasse gewechselt werden muss. 893
SmartBooks
[receiver
Objective-C und Cocoa – Band 2
Superklasse superklasse -method1 …
method2]
Basisklasse superklasse -method1 -method2 … Class
receiver
Subklasse superklasse -method2 -method3 …
Auch bei Klassenobjekten wird nach der nächsten Implementierung gesucht.
self und Nachrichten an self Da die Bindung an den Empfänger dynamisch erfolgt, muss im Hinterkopf behalten werden, dass eine Nachricht an ein Objekt einer Subklasse zugestellt werden kann. Dies ist dann der Fall, wenn eine Nachricht beim Dispatch in der Klassenhierarchie nach oben »gewandert« ist und sodann aus dieser Methode wiederum eine Nachricht an sich selbst geschickt wird. Da der Empfänger seinen Typ durch die Wanderung nicht ändert, wird nun wiederum ab der untersten Ebene nach einer Nachricht gesucht. Anders formuliert: self macht die Polymorphie nicht mit und zeigt während der gesamten Zeit auf dasselbe Empfängerobjekt, ohne dass die Klasse geändert wird. Dies gilt sowohl für self in Klassen- wie in Instanzobjekten. Dies ist von praktischer Bedeutung bei Convenience-Allocators. Wird ein solcher für die Basisklasse angeboten, so funktioniert er bei richtiger Implementierung auch für die Subklasse. // Aufruf eines Convenience-Allocators BaseClass* baseInstance = [BaseClass baseClass]; Subclass* subinstance = [Subclass baseClass];
BEISPIEL Auch wenn es in Wahrheit etwas komplexer ist (Stichwort: Class-Cluster), so existiert diese Situation etwa bei der Nachricht string, welche an die Subklasse NSMutableString gesendet wird.
894
Kapitel 10
Objective-C-Referenz
Auf den ersten Blick sieht es so aus, als ob +baseClass (BaseClass) von der Subklasse mit einem +baseClass (Subclass) überschrieben werden müsste, damit die letzte Zeile funktioniert. Tatsächlich ist dem nicht so, wenn +baseClass (BaseClass) günstig implementiert wird. Zum besseren Verständnis haben wir das Nachrichtentriplet des Convenience-Allocators einmal aufgedröselt: @implementation BaseClass + (id)baseClass { id instance = [self alloc]; instance = [instance init]; instance = [instance autorelease]; return instance; }
Da unsere Subklasse die Methode +baseClass nicht überschreibt, wird die Implementierung der Basisklasse BaseClass ausgeführt. In der entsprechenden Methode zeigt self aber immer noch auf das Klassenobjekt von Subclass! Denn bei der Methodensuche ist der Dispatcher zur Basisklasse zwar durchgefallen, er ändert jedoch nicht self und das, worauf self zeigt. NSObject superklasse +alloc … Basisklasse superklasse +baseClass … [Subclass baseClass]
Subklasse superklasse +subclass …
self
Zwar wird die Implementierung von BaseClass verwendet, jedoch zeigt self weiterhin auf das Klassenobjekt von Subclass.
Wird daher in +baseClass (BaseClass) eine Nachricht an self geschickt, so erfolgt wieder der komplette Dispatch von Subclass aus. Es sei dazu angemerkt, dass auch in diesem Falle +alloc (NSObject) als self einen Zeiger auf das Klassenobjekt von Subclass erhält. Dies führt dazu, dass automatisch eine Instanz von Subclass erzeugt wird und nicht auf jeder Ebene +alloc überschrieben werden muss. 895
SmartBooks
Objective-C und Cocoa – Band 2
NSObject superklasse +alloc …
alloc
Basisklasse superklasse +baseClass … Subklasse superklasse +subclass …
self
… [self alloc]; …
Unabhängig vom Ort der Verwendung startet die Ausführung immer bei der Empfängerklasse.
Dies ergäbe dann einen Unterschied, wenn +alloc in Subclass überschrieben würde. Es erfolgt also ein Sprung von der Basisklasse an die Subklasse. NSObject superklasse +alloc …
alloc
Basisklasse superklasse +baseClass … Subklasse superklasse +alloc +subclass … … [self alloc]; …
Es kann passieren, dass eine Nachricht an self wieder zur Subklasse springt.
Dieser Effekt des Rücksprunges existiert ebenso bei Nachrichten an Instanzen. Man macht sich diesen etwa bei Initialisierern zunutze, wenn der Designated-Initializer gewechselt wird.
896
Kapitel 10
Objective-C-Referenz
-init
NSObject superklasse -init …
NSObject superklasse -init …
Basisklasse superklasse -init -initWith: …
Basisklasse superklasse -init -initWith: …
Subklasse superklasse -initWith:… …
self
-initWith:…
Subklasse superklasse -initWith:… …
self
… [self initWith:]; …
Es reicht aus, dass die aufgerufene Methode überschrieben wird.
Statisch gebundene Nachrichten Es existieren in Objective-C zwei Fälle, in denen allerdings ein statisches Dispatching vorgenommen wird. »Statisch« bedeutet hierbei nicht, dass es keinerlei Polymorphie mehr gäbe. Sie ist lediglich wie bei C++ an die Klassenhierarchie gebunden und sucht darüber hinaus lediglich entlang der Ableitungskette nach oben. Die Folge ist also, dass nur Implementierungen gefunden werden, die sich ab dem Startpunkt der Suche oder in höheren Klassen finden lassen. Nachrichten an Klassen Wir hatten vorhin schon Nachrichten an Klassenobjekte verschickt. Objective-C kennt auch die Möglichkeit, eine Nachricht an eine im Sourcetext bezeichnete Klasse zu senden. Ändern wir obigen Code für ein Beispiel: @implementation BaseClass + (id)baseClass { id instance = [BaseClass alloc]; instance = [instance init]; instance = [instance autorelease]; return instance; }
897
SmartBooks
Objective-C und Cocoa – Band 2
Zum einen dürfte klar sein, dass das alloc nur ab BaseClass gesucht wird, also etwa eine Implementierung in einer Subklasse nicht gefunden wird. Denn wir sagen in dem Code ja explizit, dass BaseClass der Ausgangspunkt sein soll. NSObject superklasse +alloc … Basisklasse superklasse +baseClass … [Subclass baseClass]
Subklasse superklasse +alloc +subclass …
self
self
NSObject superklasse +alloc … Basisklasse superklasse +baseClass …
alloc
Subklasse superklasse +alloc +subclass … … [BaseClass alloc]; …
Das überschriebene +alloc wird nicht erreicht.
Es existiert hier aber zudem das Problem, dass self auf das Klassenobjekt von BaseClass zeigt. Denn wir geben BaseClass ja explizit als Empfänger an, und self ist immer der Empfänger. Dies gilt auch dann, wenn wir in diese Methode gekommen sind, weil ursprünglich eine Nachricht an die Subklasse gerichtet wurde. Denn da wir self nicht mehr benutzen, ist die Information über den ursprünglichen Adressaten verloren. Der Convenience-Allocator funktioniert also nicht mehr für Subklassen. In aller Regel ist es daher besser, in Klassenmethoden self zu verwenden. Rufe ich allerdings den Convenience-Allocator auf, so will ich eine bestimmte Klasse adressieren. Hier ist also die Verwendung des Typen richtig. Meist geschieht dies ohnehin in Instanzmethoden, so dass gar kein self existiert, welches auf ein Klassenobjekt zeigt. @implementation AllocatingClass - (void)instanceMethod { // self zeigt auf Instanz von AllocatingClass id instance = [BaseClass baseClass]; // Erzeugt BaseClass id instance = [Subclass baseClass]; // Erzeugt Subclass }
898
Kapitel 10
Objective-C-Referenz
Nachrichten an super Während die bisherigen Fälle nur durch angestrengtes Nachdenken zu erwartende Resultate zeigten, sorgt die Verwendung von super zu einer inhaltlich neuen Sachlage. Bei super wird self nicht geändert, sondern lediglich die Methodensuche eingeengt – und zwar auf die Klasse ab der sendenden Klasse. self wird hierbei jedoch nicht verändert. Dies bedeutet, dass bei einer erneuten Benutzung von self beim Empfänger wieder ab der ursprünglichen Klasse gesucht wird. Lautet also unser Convenience-Allocator wie folgt: + (id)baseClass { id instance = [super alloc]; instance = [instance init]; instance = [instance autorelease]; return instance; }
so würde erneut eine überschriebene Methode in Subclass und auch in BaseClass nicht gefunden, also in jedem Falle +alloc (NSObject) ausgeführt. Nur würde self weiterhin auf das Klassenobjekt von Subclass verweisen, so dass +alloc eine Instanz von Subclass erzeugen würde.
[Subclass baseClass]
NSObject superklasse +alloc …
NSObject superklasse +alloc …
Basisklasse superklasse +baseClass …
Basisklasse superklasse +baseClass …
Subklasse superklasse +alloc +subclass …
self
self
alloc
Subklasse superklasse +alloc +subclass … … [super alloc]; …
super engt die Methodensuche ein, ohne self zu modifizieren.
899
SmartBooks
Objective-C und Cocoa – Band 2
Praktisch wichtige Bedeutung hat dies allerdings bei Instanzmethoden, wenn eine Superimlementierung ausgeführt werden soll, diese aber wieder zur tiefer gelegenen Implementierung zurückspringen muss. Klassischer Fall sind Initialisiererketten: NSObject superklasse -init …
self init
Instanz isa …
Basisklasse superklasse -init -initWith:… … Subklasse superklasse -init -initWith:… …
NSObject superklasse -init …
self Instanz isa …
NSObject superklasse -init …
Basisklasse superklasse -init -initWith:… … init (super) Subklasse superklasse -init -initWith:… …
self Instanz isa …
Basisklasse superklasse -init -initWith:… … initWith:… Subklasse superklasse -init -initWith:… …
…
[super init]; …
…
[self initWith:…] …
Klassen-Ping-Pong: Was an super losgeschickt wird, kann bald retour kommen.
Dies führt dazu, dass auch bei einem Versand an super wieder Implementierungen der tieferen Klasse ausgeführt werden können, weil sich self nicht verändert. superclass Dies führt zu einer Denkfehlerquelle: Wird eine Nachricht an super gesendet, so bleibt die Klasse des Empfängers unverändert, weil der Empfänger (self) unverändert bleibt. Denken wir etwa daran, dass wir überprüfen wollen, ob eine Basisimplementierung existiert: @implementation MyClass - (void)instanceMethod { // Falls eine Basisimplementierung existiert // diese ausführen SEL selector = @selector( instanceSelector ); if( [super respondsToSelector:selector] ) { [super instanceMethod]; } … }
Auf den ersten Blick sieht der Code richtig aus. Bei näherer Betrachtung wird allerdings der if-Zweig stets ausgeführt: super verändert nur die Methodensuche. Da -respondsToSelector: jedoch ohnehin (nur) in NSObject implementiert ist, wird 900
Kapitel 10
Objective-C-Referenz
dieselbe Methode gefunden, als ob wir die Nachricht an self schicken würden. (Ein Unterschied liegt lediglich dann vor, wenn die Klasse selbst -respondsToSelector: überschreiben würde.) Die Methode in NSObject bekommt jedoch ein self, welches auf die ursprüngliche Instanz mit der ursprünglichen Klasse zeigt. Es wird also überprüft, ob MyClass -instanceMethod implementiert – was offenkundig der Fall ist. Auch eine Änderung auf die Klasse ändert daran nichts: @implementation MyClass - (void)instanceMethod { // Falls eine Basisimplementierung existiert // diese ausführen Class class = [super class] SEL selector = @selector( instanceSelector ); if( [class instancesRespondsToSelector:selector] ) { [super instanceMethod]; } … }
Denn hier wird in der ersten Zeile als Klasse MyClass zurückgegeben, weil zwar die Superimplementierung der Methode gesucht wird, self aber weiterhin auf die Instanz von MyClass zeigt. Um diese Problematik zu umgehen, existiert die Methode -superclass: @implementation MyClass - (void)instanceMethod { // Falls eine Basisimplementierung existiert // diese ausführen Class class = [self superclass] SEL selector = @selector( instanceSelector ); if( [class instancesRespondsToSelector:selector] ) { [super instanceMethod]; } … }
Hier liefert tatsächlich -superclass die Superklasse, so dass die spätere Abfrage wie erwartet funktioniert.
901
SmartBooks
Objective-C und Cocoa – Band 2
Gescheiteter Dispatch Scheitert eine Nachricht an ein Objekt, wird also weder bei der Empfängerklasse noch bei einer Superklasse hiervon eine entsprechende Methode gefunden, so wird dies standardmäßig mit einer Exception quittiert. Allerdings stammt die Exception nicht aus dem Laufzeitsystem, wie man vielleicht denken könnte. Dieses versucht vielmehr die Nachricht zu retten: Forward-Target Häufig ist der Grund für das Scheitern der Nachricht, dass die entsprechende Methode nicht in der Klasse selbst implementiert ist, sondern auf eine Helferklasse ausgelagert wurde. Die vordere Klasse dient lediglich als bekannte API der Funktionalität. Man nennt eine solche Klasse einen Proxy (Stellvertreter), da sie für die Funktionalität in der hinteren Klasse steht. Proxy -frontMethod … method
Proxy isa helper …
Helper -method …
isa
Helper …
Manchmal will man eine Nachricht durch jemand anderen bearbeiten lassen.
Da diese Situation vergleichsweise häufig auftritt, existiert seit Mac 10.6 die Möglichkeit, eine Nachricht unmittelbar weiterzuleiten. Daher fragt das Laufzeitsystem zunächst mit -forwardingTargetForSelector: nach, ob die Nachricht nicht an ein anderes Objekt zugestellt werden soll. Eine Änderung von Parametern oder des Selectors ist hierbei nicht möglich, sondern lediglich eine Änderung des Adressaten. Dabei sind mehrere Fälle zu unterscheiden:
•
Die Methode ist bereits in der Proxy-Klasse implementiert: In diesem Falle wird -forwardingTargetForSelector: erst gar nicht ausgeführt, da es sich nicht um eine gescheiterte Nachricht handelt.
•
Die Methode ist in einem Proxy implementiert: Hier ist der Proxy zurückzugeben.
•
Die Methode ist auch nicht in dem Proxy implementiert: Es ist der Rückgabewert der Superimplementierung zurückzugeben.
902
Kapitel 10
Objective-C-Referenz
Forward-Invocation Zuweilen reicht es aber nicht aus, einfach das Ziel der Nachricht zu ändern. Für diese Fälle existiert schon immer, also auch vor Snow Leopard, -forwardInvocation:. Diese Methode wird in einem nächsten Schritt aufgerufen. Sie ist teurer als das Target-Forwarding, weil eine Invocation-Instanz erzeugt wird. Diese benötigt wiederum die Methode -methodeSignatureForSelector:, weshalb diese ebenfalls überschrieben werden muss. Eine Implementierung kann alles Mögliche unternehmen, um die Nachricht zu retten. Dies ist der Methode überlassen. Naheliegend ist es freilich häufig, die übergebene Invocation-Instanz an jemand anderen zu senden, wenn Target-Forwarding nicht ausreicht. Sieht die Methode allerdings keine Möglichkeit zur Rettung, so ist -doesNotRecognizeSelector: auszuführen, damit eine Exception geworfen wird.
Dynamische Methodenerzeugung Sowohl bei dem Versand von Nachrichten als auch bei -respondsToSelector: bzw. instancesRespondToSelector: wird dem Empfänger zunächst die Möglichkeit gegeben, eine Methode anzulegen. Das Hinzufügen von Methoden funktioniert über das Laufzeitsystem.
Exceptions Eine Exception (Ausnahme) ist ein Mechanismus, mit der der übliche Programmlauf unterbrochen wird. Insbesondere wird bei verschachtelten Ausführungen von Methoden bei einer Exception nicht zur aufrufenden Stelle zurückgekehrt, sondern zum sogenannten Exceptionhandler.
AUFGEPASST Da in Objective-C Methoden nicht aufgerufen, sondern Nachrichten verschickt werden, ist der obige Satz nicht logisch, sondern technisch zu verstehen. Letztlich führt ja die Abarbeitung der Nachrichten zu einem Aufruf einer Methode – wenn auch über den Umweg des Dispatching. Auch das Dispatching kann eine Exception auslösen, nämlich dann, wenn eine passende Methode nicht gefunden wird.
903
SmartBooks
Objective-C und Cocoa – Band 2
// irgendwo … [instance message] …
-(void)message … [another doIt] …
-(void)doIt … //Exception// …
Exceptionhandler
Üblicherweise kehren Methoden zum Tatort zurück. Exceptions ändern das.
Da Exceptions recht teuer sind, sollten andere Fehlerbenachrichtigungsmethoden wie nil-Returnwert oder Rückgabe einer NSError-Instanz als doppelt indirekter Parameter bevorzugt werden.
Exceptions werfen In Cocoa werden Exceptions durch Instanzen der Klasse NSException modelliert. Diese verfügen im Wesentlichen über drei Eigenschaften:
•
name (NSString) stellt den Identifizierer der Exception dar. Es gibt eine vorgefertigte Liste von Exception-Names, die Sie bitte der Dokumentation entnehmen.
•
reason ist eine NSString-Instanz mit einer für den Benutzer lesbaren Erläuterung des Grundes für die Exception.
•
userInfo ist indessen nicht für den Benutzer vor dem Bildschrim gedacht, sondern für den Programmierer. Es enthält ein optionales NSDictionary, welches weitere Informationen enthalten kann (etwa zum Grund der Exception) oder Daten, die die Exception auslösten.
Eine Exception-Instanz wird auf übliche Weise gebaut und kann dann mit der Methode -raise – man sagt: – geworfen werden. Es existieren jedoch auch Convenience-Allocator, die gleich die Exception werfen. Seit 10.3 ist Exception-Support jedoch auch in Objective-C unmittelbar eingebaut. Hierzu dient das Schlüsselwort @throw. NSException* exception; exception = [NSException exceptionWithName:customException reason:@"Throw 1" userInfo:nil]; @throw exception;
904
Kapitel 10
Objective-C-Referenz
Der Paramter der @throw-Direktive muss nicht von der Klasse NSException oder einer Subklasse sein. Sie stellt lediglich eine Infrastruktur für die bequeme Benutzung dar.
Exceptions fangen Wie bereits erwähnt, führt eine Exception dazu, dass der Programmfluss an der Stelle des Exceptionhandlers fortgeführt wird. Hierbei sind folgende Situationen zu unterscheiden:
•
Ist kein Exceptionhandler vorhanden, so wird das Programm terminiert.
>… *** Terminating app due to uncaught exception … >… Stack:
•
Haben wir eine Anwendung, also mit GUI, so wird in der Run-Loop des Hauptprogrammes automatisch ein Exceptionhandler installiert. Dieser fängt die Exception, schreibt eine Mitteilung in die Konsole und lässt das Programm durch seine Existenz am Leben. Daher terminiert eine Exception im Main-Thread nicht die Anwendung. Sie haben das vielleicht schon erlebt, als Sie versehentlich ein Binding falsch gesetzt haben: Der Ladezyklus für den Nib-File wird beendet, das Programm läuft jedoch weiter.
•
Sie haben selbst einen (lokalen) Exceptionhandler installiert.
Ein Exceptionhandler besteht aus drei Teilen: Dem Try-Block, einem oder mehreren Catch-Blöcken und dem Finally-Block. // Code, der eine Exception werfen koennte @try { [Thrower throwsException]; NSLog( @"Nach der Exception" ); } // Auffangen der Exception @catch( NSException* exception ) { NSLog( @"Exception: %@", exception ); } // Unbedingt zu erledigen @finally { NSLog( @"In jedem Falle" ); } // Nachfolgender Code
905
SmartBooks
Objective-C und Cocoa – Band 2
try-Block Wie Sie sehen können, wird der Code, der im Verdacht steht, eine Exception zu werfen, in den Try-Block gekapselt. Ob eine Methode potentiell eine Exception wirft, können Sie der Dokumentation der Methode entnehmen, etwa -decimalNumberByDivingBy:, wenn der Operand 0 ist. catch-Block Erfolgt eine Exception, so wird der Catch-Block ausgeführt. Denkbar ist es, eine dezidierte Fehlerbehandlung zu starten oder einfach den Benutzer zu informieren. Allerdings erhält, wie oben ersichtlich, der Catch-Block einen Parameter, der bestimmt, dass nur diejenigen Excpetions gefangen werden sollen, die dem angegebenen Typen entsprechen. Daher ist es möglich, mehrere gestufte Catch-Blöcke zu haben. Da jedoch auch immer Subklassen getroffen werden, sind die Catch-Blöcke von speziell nach allgemein zu sortieren. Werden mehrere Catch-Blöcke getroffen, so wird nur der erste ausgeführt. @try { … } @catch( MyExceptionClass* exception ) { // Exceptions der Klasse MyExceptionClass } @catch( NSException* exception ) { // Exceptions der Klasse NSException } @catch( id exception ) { // Alles } @finally { … } // Nachfolgender Code
Findet sich kein passender Catch-Block, so wird die Exception an den nächst äußeren Exceptionhandler durchgereicht. Der Finally-Block des lokalen Handlers wird ausgeführt, der nachfolgende Code allerdings nicht mehr.
906
Kapitel 10
Objective-C-Referenz
finally-Block Schließlich – um nicht finally zu sagen – wird der Finally-Block ausgeführt, und zwar unabhängig davon, ob eine Exception geworfen wurde oder nicht. Man kann sich also auf die Ausführung verlassen, selbst wenn eine Exception auftrat. Dies ist etwa bedeutend, wenn Ressourcen vom System angefordert wurden, welche wieder freigegeben werden müssen. Häufigster Fall ist das Anfordern von Speicher bei einer Instanzerzeugung. Hier können wir jedoch gleich die Instanz im Autoreleasepool erzeugen.
Exceptions zurückwerfen Es kann freilich vorkommen, dass in einem Catch-Block erneut eine Exception auftritt. Dies sollte abgefangen werden. Wichtiger ist die Möglichkeit, die gefangene Exception erneut zu werfen. Hierzu kann im Catch-Block ein parameterloser @ throw eingefügt werden: @catch( NSException* exception ) { … @throw; }
Dieses @throw spuckt also die soeben gefressene Exception wieder aus. Das führt dazu, dass nunmehr ein etwaig bestehender äußerer Exceptionhandler diese Exception abarbeiten muss. Wenn keiner vorhanden ist, erfolgt eben die Reaktion wie ohne Catch-Block. Zu beachten ist aber in diesem Falle, dass der Finally-Block des Handlers noch ausgeführt wird. Etwaige Ressourcen werden also sauber freigegeben. Auch der folgende Code nach dem Exceptionhandler wird ausgeführt, da die Exception ordentlich gefangen wurde.
Objekterzeugung und –zerstörung Eigentlich ist, anders als bei anderen Programmiersprachen, die Objekterzeugung keine Frage von Objective-C. Die Sprache selbst kümmert sich weitestgehend nur um die ID eines Objektes, also den Zeiger hierauf. Dennoch ist der sachliche Zusammenhang so eng, dass es inhaltlich zur Sprachbeschreibung gehört – wenn auch nicht formal.
907
SmartBooks
Objective-C und Cocoa – Band 2
Klassenobjekte Klassenobjekte werden vom Laufzeitsystem erzeugt. Sie dürfen von diesem erst bei Bedarf, also beim ersten Ansprechen der Klasse, angelegt werden.
Allokation Die Nachricht +load wird versendet, wenn ein Image – Framework oder Bundle – geladen wird. Da alle enthaltenen Klassen gleichzeitig geladen werden, sind alle anderen Klassen dieses Images bereits vorhanden und benutzbar, haben jedoch möglicherweise ihrerseits noch nicht +load erhalten.
Initialisierung Wesentlich wichtiger ist +initialize. Diese Nachricht erhält jede Klasse vor ihrer erstmaligen Benutzung. Dabei gelten einige Regeln:
•
In +initialize darf nicht die super-Implementierung aufgerufen werden. Darum kümmert sich das Laufzeitsystem bei entsprechendem Bedarf.
•
Wird nicht nur die Klasse, sondern auch ihre Superklassen erstmalig benutzt, so erhalten diese ebenfalls eine initialize-Message, und zwar die jeweilige Superklasse zuerst. (Deshalb auch auch die erste Regel.)
•
Wird eine Subklasse einer bereits benutzten Klasse erstmalig benutzt, so erhält die Basisklasse nicht erneut initialize.
•
Wird eine weitere Subklasse einer bereits benutzten Klasse erstmalig benutzt, so erhält die Basisklasse ebenfalls nicht erneut ein initialize.
•
Wird eine Subklasse einer bereits benutzten Klasse erstmalig benutzt, welche +initialize nicht implementiert, so fällt die Nachricht an die Basisklasse durch. Instrument +initialize …
[Instrument initialze]
Guitar +initialize …
[Piano initialze]
Die Situationen der Initialisierung von Klassenobjekten
908
Kapitel 10
Objective-C-Referenz
Gerade der letzte Punkt ist fehlerträchtig, da auf diese Weise das Basisklassenobjekt »virtuell« zweimal +initialize ausührt, nämlich einmal als wirkliche Initialisierung und einmal als Initialisierung des Subklassenobjektes. Um die doppelte Initialisierung zu vermeiden, empfiehlt es sich, den weiteren Fall abzufangen. Hierbei kann ausgenutzt werden, dass self bei einer durchgefallenen Nachricht auf den ursprünglichen Empfänger zeigt: @implementation BaseClass + (void)initialize { if( self == [BaseClass class] ) { // Hier initialisieren } }
Zerstörung Klassenobjekte sind Singletons mit unendlicher Lebensdauer. Es existiert daher keine Methode mit Bezug zur Objektzerstörung, da diese ohnehin nie ausgeführt würde.
Instanzerzeugung Die Erzeugung von Instanzen in Objective-C ist in zweierlei Hinsicht bemerkenswert: Zum einen ist der vollständige Vorgang eine Frage des Frameworks Cocoa und nicht der Sprache Objective-C. Zum anderen werden zwei Nachrichten verschickt, nämlich +alloc zur Bereitstellung der Instanz (des von ihr belegten Speichers) und -init… zur Initialisierung der Instanz.
Allokation Bereits in NSObject ist mit +alloc eine Methode implementiert. Sie schickt an self die Nachricht +allocWithZone:, die den nötigen Speicher für eine Instanz vom System holt. Man kann diese Methode überschreiben, um die Speicheranforderung zu ändern. Dies ist für den Anwendungsprogrammierer selten notwendig. Allerdings seien drei Fälle genannt, die Apple glücklicherweise mit vollständigem Code dokumentiert hat:
•
Singletons: Es soll grundsätzlich nur eine Instanz einer Klasse erzeugt werden. Wenn man so will, ist also nur die erste Allokation wirksam. Alle weiteren
909
SmartBooks
Objective-C und Cocoa – Band 2
führen dazu, dass die ursprüngliche Instanz geliefert wird anstelle der neuen. +sharedApplication ist etwa ein Singleton der Klasse NSApplication.
•
Twintons/Multitons: Es sollen Instanzen mit gleichen Eigenschaften mehrfach verwendet werden. Erzeugen Sie etwa eine NSNumber-Instanz mit dem Ganzzahlwert 3, so wird dieser angelegt. Machen Sie dies erneut, so erhalten Sie keine neue Instanz, sondern die alte mit dem Wert 3. Sie wird wiederverwendet. Twintoning ist meist ein Implementierungsdetail, welches Sie gar nicht bemerken.
•
Class-Cluster: Manchmal ist es sinnvoll, die Klasse einer Instanz abhängig von ihrer Initialisierung zu machen. Soll etwa ein Array erstellt werden, so mag es bessere Implementierungen für Arrays mit wenigen Elementen und für solche mit vielen Elementen geben. Hierzu baut man sich entsprechende Subklassen. Die Allokation funktioniert hier anders, da bei dem +alloc noch nicht entschieden werden kann, welche Klasse die erzeugte Instanz haben soll. Es wird daher zunächst nur ein Placeholder erzeugt und erst bei der Initialisierung die wirkliche Instanz. Aus diesem Grunde ist es übrigens wichtig, den Rückgabewert eines Initialisierers zu beachten.
Bereits im +allocWithZone: wird dafür gesorgt, dass der isa-Zeiger des Objektes ordnungsgemäß gesetzt ist, so dass die Instanz Nachrichten empfangen kann. Dies ist erforderlich, weil die nachfolgende Initialisierung ja mittels einer init…-Nachricht losgetreten wird. Außerdem werden sämtliche Instanzvariablen auf den Wert 0 gesetzt, was auch immer das für den entsprechenden Datentypen bedeutet:
• • • • •
Instanzzeiger zeigen auf nil; Klassenzeiger zeigen auf Nil; Ganzzahlvariablen haben den Wert 0; Fließkommavariablen haben den Wert 0.0; Für Strukturen gilt Entsprechendes für ihre Komponenten;
Initialisierungsreihenfolge Da die Erzeugung von Instanzen nicht Bestandteil des Sprachstandards ist, muss dies von dem Programmierer selbst erledigt werden. Üblicherweise ist die Festlegung, wann was initialisiert wird, in den Sprachstandards außerordentlich komplex und mit Fallstricken übersät. Auch in Objective-C sind entsprechende Regeln zu beachten:
910
Kapitel 10
Objective-C-Referenz
Namensregeln Initialisierer tragen den Namen init… Je nach Parameteranzahl und Verwendung sollten klar beschreibende Parameternamen mit Präpositionen wie with, for usw. verwendet werden. Designated-Initializer Zu jeder Klasse existiert ein – in seltenen Fällen mehrere – Designated-Initializer, der die inhaltliche Initialisierung vornimmt. Außerdem rufen die Designated-Initializer – und nur sie – die Initialisierung in der Basisklasse auf. Typischerweise nimmt hierbei die Parameterzahl ab, da ja in der Ableitung Eigenschaften hinzugefügt sein können. Sie sind meist diejenigen Initialisierer, die die meisten Parameter erhalten. - (id)initWithName:(NSString*)initName andAge:(NSInteger)initAge { self = [super initWithName:initName]; if( self ) // Inhaltliche Intialisierung } return self; }
Secondary-Initializer Daneben können weitere Initialisierer existieren, insbesondere solche, die weniger Parameter erhalten und daher die Instanz mit Defaultwerten vorsetzen. Secondary-Initializer rufen ihren Designated-Initializer mit eben diesen Defaultwerten auf. - (id)initWithAge:(NSInteger)initAge { return [self initWithName:@"" andAge:initAge]; }
Wechsel des Designated-Initialisierers Wechselt der Designated-Initializer in einer Ableitung, so muss in der Subklasse der Designated-Initializer der Basisklasse überschrieben werden, um ihn wie einen Secondary-Initializer zu implementieren (Weiterleitung an den – neuen – Designated-Initializer). Für Secondary-Initializer der Basisklasse gilt dies nicht, da diese ja ohnehin den – überschriebenen – Designated-Initializer aufrufen. Das spart deutlich Arbeit, da die Anzahl der Initialisierer recht groß sein kann.
911
SmartBooks
Objective-C und Cocoa – Band 2
Inhaltliche Initialisierung Wie bereits ausgeführt, wird in einem Designated-Initializer die inhaltliche Initialisierung vorgenommen. Dabei existieren zwei Strategien: Sofortige Initialisierung Die einfachere Art der Implementierung liegt darin, alle Instanzvariablen auf sinnvolle Werte vorzusetzen. Hierbei stellt sich die Frage, ob auf die Instanzvariablen unmittelbar zugegriffen werden sollte oder ob auch hier Setter benutzt werden. Diese Frage wurde erstmalig im grauer Vorzeit aufgeworfen und seitdem heiß diskutiert. Wie Sie den Code-Beispielen in diesem Buch entnehmen können, gehören wir zu der moderneren Schule, die die Meinung vertritt, dass Accessoren anzuwenden sind. Zunächst: In gefühlten 99,88756 Prozent aller Fälle ist dieser Streit ohne jede Bedeutung für gar nichts. Dennoch: Als Nachteile bei Verwendung von Accessoren werden genannt:
•
»Die Instanz ist noch nicht initialisiert, darf daher keine Nachricht erhalten.« Dies ist so jedenfalls falsch, da sie ja bereits gesetzt ist und im Übrigen die Instanz bereits eine Nachricht erhielt: Mindestens ein init… (sonst wären Sie ja nicht in der Initialisierungsmethode). Auch ansonsten scheut man sich nicht davor, in der Initialisierung Nachrichten an sich selbst zu senden, etwa um Berechnungen für Startwerte durchzuführen – oder bei der Weiterleitung des Secondary-Initializers an den Designated-Initializer.
•
»Die Verwendung von Settern in einer Basisklasse führt dazu, dass in einer – später programmierten – Subklasse diese bereits aus der Subklasse ausgeführt werden, wenn die Setter überschrieben wurden. Dies ist fehlerträchtig, da der Programmierer der Subklasse möglicherweise an diesen Fall nicht dachte.« Dies ist ohne jeden Zweifel richtig, zeugt aber von einem Fehler des Programmierers, nicht des Musters. Der Effekt, dass Basisklassen in Subklassen zurückspringen, ist nichts Besonderes, wie wir sahen. Die Instanz ist auch nicht gänzlich uninitialisiert, sondern mit 0-Werten vorgesetzt. Mit einem solchen Zustand muss der Setter ohnehin zurechtkommen, wenn er einen kümmerlichen Rest von Anspruch an die Robustheit seiner Implementierung erheben möchte.
Nachteile keiner Verwendung von Accessoren (also Vorteile für die Verwendung) sind:
•
Bei einer Ableitung darf auf Instanzvariablen der Basisklasse ganz sicher nur mit Accessoren zugegriffen werden. Alles andere ist wegen Verstoßes gegen die Kapselung gewiss falsch. Wer hier einen unmittelbaren Zugriff empfähle – uns ist niemand bekannt –, realisiert die Gefahr des White-Boxings. Fallunterscheidungen sind aber immer verkomplizierend. 912
Kapitel 10
Objective-C-Referenz
•
Die unmittelbare Zuweisung an die Instanzvariable hängt von der Semantik der Eigenschaft ab: Ist dort copy versprochen, so ist in der Zuweisung ein copy zu verwenden. Ist dort retain versprochen, so ist ein retain zu verwenden. Ist dort assign versprochen, so ist unmittelbar zuzuweisen. Diese Fallunterscheidungen sind originäre Aufgabe des Setters, weshalb unklar bleibt, wieso dies in dem Initialisierer wiederholt werden muss. Eine Änderung der Setter-Semantik führt dann nur zum Erfordernis, den Initialisierer zu warten.
•
Setter führen manchmal zusätzliche Arbeiten aus, die für eine innere Konsistenz der Instanz sorgen. Diese Arbeit wäre im Initialisierer erneut zu programmieren. Das führt zu Wartungsproblemen. Dass in der Instanz noch 0-Werte vorgesetzt sein können, ist kein Nachteil, da damit die Implementierung aus Gründen der Robustheit ohnehin fertig werden muss.
Entscheiden Sie sich selbst, kloppen Sie sich mit anderen in Foren und auf Mailinglisten. Späte Initialisierung Eine andere Technik liegt darin, die Instanzvariablen, die IDs enthalten, zunächst auf nil zu belassen. Ein entsprechendes Defaultobjekt wird erst dann erzeugt, wenn der Getter erstmalig aufgerufen wird: - (NSNumber*)age { if( age == nil ) { // Defaultwert age = [[NSNumber alloc] initWithInteger:3]; } return [[age retain] autorelease]; }
Dies hat den Vorteil der Speicherersparnis, wenn viele Instanzen alloziert werden. Aber bedenken Sie bitte, dass gerade im vorstehenden Beispiel NSNumber immer dieselbe Instanz zurückgeliefert wird (Twinton), also gar keine Ersparnis erfolgt. Bei Immutable-Instanzen führt dies also zu nichts, womit die meisten Fälle abgehakt wären.
Zerstörung Instanzen können über einen begrenzten Lebenszyklus verfügen. Es kann also zu der Situation kommen, dass sie wieder gelöscht werden sollen. Dies hängt freilich vom Speicherverwaltungssystem ab:
913
SmartBooks
Objective-C und Cocoa – Band 2
Reference-Counting Arbeiten Sie mit Reference-Counting, so sind alle Verweise in Instanzvariablen zu löschen. Hierzu wird die Methode -dealloc ausgeführt. Auch hier kann man sich wiederum darüber streiten, auf welche Weise dies erfolgen soll, also über Setter mit dem Parameter nil oder über retain. Letzteres darf allerdings nur verwendet werden, wenn der Setter retain oder copy als Speicherverwaltungssemantik hatte. Bei einem assign führt dies ansonsten zu einem Dangling-Pointer. Am Ende von -dealloc ist die Superimlementierung aufzurufen, um auch der Basisklasse die Möglichkeit zu geben auszuräumen. -dealloc (NSObject) gibt dann den Speicher frei. Es ist nicht garantiert, dass -dealloc aufgerufen wird. Beim Programmende kann das System einfach alle noch bestehenden Instanzen wegwerfen, ohne diese darüber zu unterrichten. Es sollte hier also, wenn irgendwie möglich, nichts Funktionales geschehen. Diese Methode dient der Einhaltung des Reference-Countings, nicht dazu, bunte Lämpchen blinken zu lassen. Garbage-Collection Da bei Garbage-Collection das System über die Freigabe entscheidet, existiert keine Notwendigkeit für -dealloc. Es wird daher nicht ausgeführt. Ausnahmsweise notwendige Funktionalität kann in der Methode -finalize implementiert werden. Sobald Sie das »e« von »finalize« getippt haben, sollten Sie noch einmal sehr, sehr in sich gehen, um die Notwendigkeit zu überdenken.
Laufzeitsystem Apple hat mit Objective-C 2.0 eine eigene definierte Abstraktionsschicht eingeführt, mit der endlich ein vereinbarter und damit verlässlicher Zugriff auf die Laufzeitumgebung (Runtime-Envirement, RTE) möglich ist. Dies soll hier kurz angerissen werden, weil Sie so Objective-C und auch bestimmte Technologien von Cocoa wie Key-Value-Observing besser verstehen können. Außerdem kann es für sehr versierte Programmierer eine Fundgrube für ganz außergewöhnliche Lösungen sein, wie etwa die soeben angesprochene dynamische Methodenerzeugung.
Überblick Im Prinzip sind (fast) alle Informationen, die wir im Sourcetext haben - mit Ausnahme der Anweisungsfolge in den Methoden - auch zur Laufzeit verfügbar. Man 914
Kapitel 10
Objective-C-Referenz
kann also zur Laufzeit abfragen, welche Instanzvariablen vorhanden sind, welche Instanzmethoden, Klassenmethoden usw. Im Wesentlichen wird nämlich vom Compiler lediglich aus der Objective-C-Source eine Folge von C-Funktionen gemacht, die dann zur Laufzeit ausgeführt werden. So wird aus dem Versand einer Nachricht eben der Funktionsaufruf objc_msgSend() und nicht »geheimer Compilercode«. Da uns aber ebenso die Laufzeitbibliothek zur Verfügung steht, können wir ebenso diese Funktion benutzen. Es ist also ohne Weiteres möglich, aus reiner C-Source eine Nachricht an ein Objective-C-Object zu versenden. Oder anders: Im Prinzip können wir auf der Objective-Ebene von Objective-C alles machen, was auch der Compiler kann. Die Header des Laufzeitsystems finden sich in objc/ runtime.h. Grundsätzlich lassen sich drei Bereiche unterscheiden:
• • •
Globale Informationen Klassen- und objektbezogene Informationen Protokollbezogene Informationen
Hierbei ist mit Informationen nicht nur eine Liste von Eigenschaften gemeint, sondern auch bestimme Tätigkeit, wie das Versenden einer Nachricht oder das Hinzufügen einer Methode.
Instanz- und Klasseninformationen Die Informationen zu den einzelnen Klassen werden von der Laufzeitumgebung in einer bestimmten Struktur gehalten. Diese kennt im Wesentlichen drei Typen: Instanzobjekte, Klassenobjekte und Metaklassenobjekte. Diese Information ist also im Laufzeitsystem anders als im Sourcecode strukturiert, da wir dort keine Metaklassenobjekte kennen. Wie wir sehen werden, dient dies der Vereinfachung. Die Informationsstruktur wird jedoch nicht beim Programmstart fertig hingestellt. Vielmehr wird erst bei Bedarf eine Beschreibung einer Klasse in den Hauptspeicher geladen. Allerdings werden derzeit alle Klassen eines Images gleichzeitig geladen. Wird allerdings etwa ein Framework erst nach einiger Zeit benutzt, so werden dessen Klassen erst dann geladen. Eine Liste sämtlicher geladener Klassen lässt sich aus dem globalen Pool mittels objc_getClassList() entnehmen.
915
SmartBooks
Objective-C und Cocoa – Band 2
Instanzobjekte Instanzobjekte enthalten die konkreten Werte der Instanzvariablen. Hierzu zählen auch Verwaltungsinformationen wie der Reference-Count und isa, welche auf das Klassenobjekt verweist. Die Funktionen des Laufzeitsystems, welche sich mit Instanzen befassen, lauten auf object_… Sie betreffen teilweise auch Klassenobjekte. Es existiert keine globale Halde aller Instanzobjekte. Lebenszyklus Eine Instanz kann mit der Cocoa-Methode +allocWithZone: erstellt werden. – Oder mit class_createInstance(). (Die Instanzerzeugung ist eigentlich eine Aufgabe der übergeordneten Klasse.) In NSFoundation existiert ein Pendant NSAllocate Object(). Die beiden letzten Varianten haben den Vorteil, dass für jede neu erzeugte Instanz isoliert bestimmt werden kann, dass eine bestimme Menge an Speicher zusätzlich angelegt wird. Gelöscht wird eine Instanz mit object_dispose() oder mittels der FoundationFunktion NSDeallocateObject(), was ziemlich gefährlich ist, da die Instanz wirklich gelöscht wird, also auch dann, wenn der Reference-Count nicht auf 0 steht. Reference-Counting ist eine Frage von Cocoa, nicht des Laufzeitsystems. Eine flache Kopie kann mit object_copy() erzeugt werden. Dies ist ebenso gefährlich wie die Verwendung des Foundation-Pendants NSCopyObject(). Zugriff auf Instanzvariablen Das Laufzeitsystem lässt den unmittelbaren Zugriff auf Instanzvariablen zu, auch dann, wenn diese nicht als public definiert sind. Zum lesenden Zugriff mittels des Variablennamens existiert object_getInstanceVariable(). Hier wird das Ergebnis über die Parameterliste zurückgegeben. Als Returnwert erhält man eine Instanz der opaquen Struktur Ivar. Daneben existiert für Zugriffe auf ID-Instanzvariablen die Funktion object_getIvar(), welches eben diese Ivar-Struktur als Parameter verlangt. Dies ist schneller. Man kann die Ivar-Struktur auch losgelöst mittels class_getInstanceVariable() holen. Für den schreibenden Zugriff gilt Entsprechendes mit den Funktionen object_set InstanceVariable() und object_setIvar(). Extra-Bytes Wird mittels der Laufzeitfunktion class_createInstance() anstelle mit +alloc eine Instanz erzeugt, so kann pro Instanz ein Extra-Speicher definiert werden. Auf diesen Extra-Speicher kann mittels object_getIndexedVars() zugegriffen werden. 916
Kapitel 10
Objective-C-Referenz
Assoziationen Mit Mac OS X 10.6 ist es möglich, an jedes Objekt in ein angehängtes Dictionary andere Objekte zu kleben. Hierzu dienen die Funktionen object_setAssiociatedObject(), object_getAssociatedObject() und object_removeAssociatedObject(). Wie auch bei Propertys können dabei Speicherverwaltungsregeln (Policy) bestimmt werden, die assign, retain bzw. copy entsprechen und zudem bis auf assign jeweils eine nonatomic-Variante kennen. Ermittlung des Klassenobjektes Es gibt prinzipiell zwei Möglichkeiten, von einem Instanzobjekt an das Klassenobjekt zu gelangen. Die eine hatten wir bereits kennengelernt: -class. Das Laufzeitsystem bietet demgegenüber die Funktion object_getClass(). Und dies ist nicht dasselbe: -class ist eine x-beliebige Methode wie jede andere. Es kann also die Methode in einer Klasse überschrieben worden sein, um eine andere Klasse vorzutäuschen. Hiervon macht Cocoa tatsächlich Gebrauch, wenn es spezialisierte Unterklassen verwendet oder gar erst zur Laufzeit passende Klassen erstellt. Erst object_getClass() des Laufzeitsystems liefert die wahre Klasse. Sie sollten in der Regel die offizielle Klasse respektieren, also die Objective-C-Methode -class verwenden.
Klassenobjekte Da für alle Instanzen einer Klasse der Satz an Instanzmethoden ebenso fest ist wie die Beschreibung der Instanzvariablen, wird diese Information vom Laufzeitsystem in das Klassenobjekt ausgelagert. So wird es ja auch von uns in die Source eingegeben. Die Methoden, die sich auf Klassenobjekte beziehen, hören auf class_… oder object_…, wenn dies bei Klassenobjekten sinnvoll ist. Lebenszyklus Klassen werden mit objc_allocateClassPair() erzeugt und sodann mit objc_registerClassPair() im Laufzeitsystem registriert. (Die Instanzerzeugung ist eigentlich eine Aufgabe des übergeordneten globalen Systems.) Hiernach darf die Klassenbeschreibung nicht mehr verändert werden. Klassenobjekte werden auch im RTE nicht zerstört. Superklasse Die Superklasse muss bereits bei Erzeugung angegeben werden. Man kann sie mit class_getSuperclass abfragen.
917
SmartBooks
Objective-C und Cocoa – Band 2
Instanzvariablen, Propertys und Protokolle Vor der Registrierung der Klasse können Instanzvariablen der Klasse hinzugefügt und entfernt werden. Danach können auch die Beschreibungen abgeholt werden. Entsprechendes gilt für Propertys und Protokolle. Für alle drei Arten existieren Funktionen, die gleich eine vollständige Liste aller Einträge liefern, etwa class_copyIvarList(). Funktionen, die ein copy im Namen enthalten, liefern eine Kopie der internen Information. Der Aufrufer der Funktion ist daher dafür verantwortlich, den Speicher mittels free() wieder freizugeben. Instanzmethoden Das Klassenobjekt enthält auch die Liste der verfügbaren Instanzmethoden, die vor Registrierung verändert werden kann. Die Beschreibung zu einer Methode kann jederzeit mittels class_getInstance Method() abgeholt werden. Dabei wird notfalls in Superklassen nach der entsprechenden Methode gesucht. Der Returnwert ist eine opaque Struktur des Type Method. Es existiert ein Satz von Funktionen, deren Name mit method_ beginnt, die dann weitere Auskunft über Returntyp, Anzahl der Parameter usw. geben können. Außerdem kann mit method_getImplementation() hieraus der Funktionszeiger ermittelt werden, was auch unmittelbar durch class_GetMethodImplementation() erfolgen kann. Außerdem können gleich alle Beschreibungen der Instanzmethoden einer Klasse mit class_copyMethodList() kopiert werden, wobei auch hier wieder die Verantwortung für die Freigabe beim Aufrufer liegt. Außerdem wird nicht in Superklassen nach Methoden gesucht, so dass sich in dieser Liste nur in der Klasse neue oder überschriebene Methoden finden. Es kommt für einen Eintrag in der Liste nur darauf an, ob diese tatsächlich implementiert wurde. Ob sie im Header deklariert wurde, aus einem Protokoll entstammt oder überhaupt keine Deklaration für sie existiert, ist unerheblich. Klassenmethoden Für die Suche nach Methoden existiert noch die Funktion class_getClassMethod(). Wir werden sehen, dass allerdings die Liste der Klassenmethoden woanders verortet ist und diese Funktion nur der Bequemlichkeit dient.
Metaklassenobjekte Wenn Sie sich noch einmal die Graphiken zum Message-Dispatching in Objective-C anschauen, wird Ihnen vielleicht eine Ungereimtheit auffallen: Um die Methode für eine Nachricht an eine Instanz zu finden, muss das Laufzeitsystem zunächst 918
Kapitel 10
Objective-C-Referenz
mittels des isa-Zeigers nach dem Klassenobjekt suchen und sodann die Klassenhierarchie hochlaufen, bis eine entsprechende Methode gefunden worden ist. Ist indessen der Empfänger einer Nachricht ein Klassenobjekt, so kann unmittelbar ab dem Empfänger die Liste der Klassenmethode durchsucht werden, wobei auch hier in der Hierarchie nach oben gewandert wird. Der Dispatch von Klassenmethoden besitzt also eine Indirektion weniger. Die gilt übrigens auch ansonsten: Um zu erfahren, ob eine Instanz eine Methode besitzt, muss ich zunächst zu ihrer Klasse wechseln und dort nach der Methode suchen. Bei einer Klasse erübrigt sich der Wechsel, weil Klassen selbst ihre Klassenmethoden kennen. Diesen Unterschied beseitigt das Laufzeitsystem, indem es eine weitere Ebene einführt: das Metaklassenobjekt. Das Metaklassenobjekt ist nichts anderes als die Klasse der Klasse. Es lässt sich daher ebenso wie die Klasse aus einer Instanz mittels object_getClass() ermitteln: id instance = … Class class = object_getClass( instance ); Class metaclass = object_getClass( class );
Wird also nach einer Klassenmethode gesucht, weil der Empfänger einer Nachricht eine Klasse ist, so wird wieder dem isa-Zeiger zum Metaklassenobjekt gefolgt und dort die Suche begonnen. Die Situation entspricht wieder derjenigen, die bei Instanzmethoden besteht.
isa
Instanz …
Subklasse superclass isa Instanzmethoden …
Instrument (Meta) superclass isa Klassenmethoden …
class_getSuperclass
Basisklasse superclass isa Instanzmethoden …
Metasubklasse superclass isa Klassenmethoden …
object_getClass
Jedes Klassenobjekt hat ein Metaklassenobjekt mit der Klassenmethode.
919
SmartBooks
Objective-C und Cocoa – Band 2
Metaklassen werden stets gemeinsam mit den Klassen als Class-Pair angelegt und registriert. Sie enthalten keine Informationen über Instanzvariablen, da dies bedeuten würde, dass Klassenobjekte über Instanzvariablen verfügen.
Protokolle Klassen können Protokolle implementieren. Diese werden entsprechend in einer Protokollliste vermerkt. Eine Eigenart ergibt sich daraus, dass Protokolle sowohl zu Klassen gehörige Instanzmethoden als auch zu Metaklassen gehörige Klassenmethoden enthalten können. Sie werden daher stets von beiden referenziert. Womit wir beim nächsten wichtigen Punkt wären: Methodenliste, Instanzvariablenliste und Eigenschaftsliste gehören der Klasse. Sie sind also Bestandteil. Protokolle werden dagegen von Klassen implementiert. Sie sind also nicht Bestandteil einer bestimmten Klassendefinition, sondern durch die Klasse referenziert. Dabei können Protokolle wiederum Eigenschaften und Methoden enthalten. Hierbei handelt es sich aber nicht um eine Vererbungsstruktur, sondern um reine Verweise. Es ist nicht möglich, bei einem Protokoll die Methoden als Variable des Typs Method abzufragen, weil Protokolle anders als Klassen keine Methoden implementieren. Vielmehr zeigen sie ja nur die Notwendigkeit einer Methodenimplementierung an. Daher lassen sich nur Methodenbeschreibungen des Typs struct objc_method_description extrahieren. Während die von einer Klasse implementierten Protokolle dort abgefragt werden können, erlaubt es der globale Informationspool ebenfalls, sämtliche Protokolle mittels objc_copyProtocolList zu holen.
Nachrichtenversand Bei der Übersetzung der Sourcen macht der Compiler aus den Nachrichten mit eckigen Klammern Funktionsaufrufe in die Laufzeitumgebung. Diese übernehmen auch das Dispatching. Im Abschnitt über das Dispatching wurde bereits ausgeführt, dass es grundsätzlich zwei Möglichkeiten des Dispatches gibt: statisches super-Dispatching und dynamisches Dispatching mit einem Zeiger auf einen Empfänger. Dementsprechend existieren zwei Funktionssätze: objc_msgSend und objc_msgSendSuper. In beiden Sätzen befinden sich eine jeweils gleichnamige Grundfunktion sowie zwei Spezialfunktionen: 920
Kapitel 10
Objective-C-Referenz
•
objc_msgSend_fpret() bzw. objc_msgSendSuper_fpret() sind zu benutzen, wenn der Rückgabewert der Methode eine Fließkommazahl ist. Zwingend ist das aber lediglich, wenn die Applikation auf Intelprozessoren ausgeführt wird, da dort die Rückgabe inkompatibel zu integralen Werten (int, char usw.) erfolgt. Da Sie jedoch sicherlich nicht mehr nur PPC unterstützen wollen, ist diese Funktion generell zu verwenden.
•
objc_msgSend_stret() bzw. objc_msgSendSuper_stret() sind zu benutzen, wenn eine Struktur den Rückgabewert bildet. Der Grund dafür ist, dass in Objective-C Strukturen by-Value behandelt werden. Wenn eine Methode eine solche liefert, müsste objc_msgSend() wieder diese entgegennehmen und ihrerseits ausliefern. Das ist aber nicht möglich, weil objc_msgSend() für sämtliche Methoden gilt, also alle möglichen Strukturgrößen behandeln müsste. objc_msgSend_stret() bewerkstelligt die Rückgabe über einen Zeiger in der Parameterliste und funktioniert damit für alle Strukturgrößen.
Zusammenfassung Die Referenz hat kurz, dafür aber umfassend die Sprache Objective-C und grundlegende Elemente von Cocoa erläutert. Sie dient vor allem zum schnellen Nachschlagen, falls etwas unklar ist. Wir haben ferner einen kleinen Einblick in die Laufzeitumgebung von Objective-C gewährt. Vielleicht ging es Ihnen so wie uns, dass gleich Ideen entstanden, wie man Dinge in Objective-C performanter implementieren kann. So arbeiten wir etwa derzeit an einem spät-bindenden Framework für Aspekt-orientierte Programmierung (AOP) in Objective-C, welches ganz ohne Austausch des Compilers oder Code-Generators auskommt. Die Möglichkeiten sind tatsächlich unübersehbar. Man muss nur Ideen haben, die sich meist dann einstellen, wenn man sich ärgert, dass sich etwas in Objective-C nicht formulieren lässt. An diesem Punkt sollten Sie an das Laufzeitsystem denken.
921
SmartBooks
Objective-C und Cocoa – Band 2
922
Index
SmartBooks
Objective-C und Cocoa – Band 2
Verzeichnis der Schlüsselwörter und Bezeichner + (Additionsoperator)....................................857 & (Adressoperator)........................................862 () (Anonyme Kategorie)................................874 [] (Arraytyp)....................................................844 , (Aufzählungsoperator)................................863 () (Ausführungsoperator)..............................865 ?: (Bedingungsoperator)................................863 __block.................................................... 855, 887 ^ (Block)..........................................................885 ^(Blocktyp).....................................................840 () (Castingoperator).......................................855 @catch()...........................................................905 @class...............................................................874 #define..............................................................763 --(Dekrementierungsoperator)....................857 -> (Dereferenzierungsoperator)...................862 [] (Dereferenzierungsoperator)....................862 * Dereferenzierungsoperator).......................862 / (Divisionsoperator).....................................857 @dynamic........................................................876 #elif...................................................................766 #else..................................................................766 #endif...............................................................766 @executable_path...........................................795 @finally................................................... 905, 907 #if......................................................................766 #ifdef................................................................769 ++ (Inkrementierungsoperator)...................857 @interface........................................................867 () (Klammeroperator)....................................865 . (Komponentenoperator).............................862 ! (logischer Nicht-Operator).........................858 | (logischer Oder-Operator)..........................858 & (logischer Und-Operator).........................858 % (Modulooperator)......................................857 * (Multiplikationsoperator)...........................857 [] (Nachricht)..................................................888 @optional................................................ 353, 878 @package.........................................................869 @private...........................................................869 @property........................................................871 @protected.......................................................869 @protocol............................................... 838, 879 (Protokoll)........................................ 868, 879 @public............................................................869
924
@required........................................................878 - (Subtraktionsoperator)................................857 @synchronized................................................735 @synthesize............................................ 872, 875 @try..................................................................905 != (Vergleichsoperator)..................................858 < (Vergleichsoperator)...................................858 (Vergleichsoperator)...................................858 >= (Vergleichsoperator)................................858 - (Vorzeichenoperator)..................................857 + (Vorzeichenoperator).................................857 * (Zeigertyp)....................................................837 = (Zuweisungsoperator)................................864 assign................................................................872 auto...................................................................850 BOOL...............................................................831 break........................................................ 882, 884 CAAction.........................................................467 CAAnimation.................................................467 -setDuration:............................................476 CAAnimationGroup......................................468 CABasicAnimation........................................468 CAConstraintLayoutManager......................498 CAContraint...................................................499 +contraintWithAttribute: relativeTo: attribute:scale:offset:...................499 CAKeyframeAnimation................................468 CALayer...........................................................469 -drawLayer: inContext:...........................................493 -setDelegate:.............................................493 CAMediaTiming............................................467 CAMediaTimingFunction.............................467 CAPropertyAnimation..................................467 +animationWithKeyPath:.......................468 CAScrollLayer.................................................469 case...................................................................882 CATextLayer....................................................469 CATilesLayer...................................................469 CATransition...................................................467 -setSubtype:..................................... 467, 477 -setType:....................................................467 CGFloat............................................................836 char...................................................................830 CIColor............................................................442 CIContext........................................................457
Index
CIFilter.............................................................442 +filterWithName:............................ 447, 454 -viewForUIConfiguration: excludeKeys:.......................................462 CIFilterGenerator...........................................462 -writeToURL: atomically:..........................................462 CIImage...........................................................442 -extent........................................................450 +imageWithData:.....................................448 CIVector...........................................................442 Class.................................................................838 const.................................................................849 continue...........................................................884 copy..................................................................872 default..............................................................882 defined() (Präprozessor)................................769 do......................................................................883 double...............................................................836 else....................................................................881 enum................................................................841 extern...............................................................853 float...................................................................836 for.....................................................................883 for in.................................................................884 FourCharCode................................................831 fpos_t...............................................................831 getter=..............................................................873 IBOutlet...........................................................868 id.......................................................................838 if........................................................................881 IKFilterBrowserPanel.....................................462 IMP...................................................................839 int......................................................................830 long...................................................................830 long double......................................................836 long long..........................................................830 nil......................................................................838 Nil.....................................................................838 NO....................................................................831 nonatomic........................................................874 NSAffineTransform..........................................89 -appendTransform:....................................97 -concat.................................................. 90, 95 -prependTransform:..................................97 -rotateByDegrees:.......................................89 -scaleXBy: yBy:........................................................97 +transform..................................................89 -translateXBy: yBy:........................................................95
NSAllDomainMask........................................570 NSAnimatablePropertyContainer -animationForKey:..................................466 +defaultAnimationForKey:....................466 NSAnimationContext....................................506 NSAppleScript................................................642 -executeAndReturnError\......................642 -initWithSource\......................................642 NSAppleScriptEnabled..................................632 NSApplication.................................................635 -activateIgnoringOtherApps\.................601 -beginModalSessionForWindow:..........717 -cancelUserAttentionRequest\...............603 -deactivate\...............................................601 -dockTile...................................................589 -endModalSession:..................................717 NSCriticalRequest....................................603 NSInformationalRequest........................602 NSRunContinuesResponse.....................718 NSTerminateCancel.................................604 NSTerminateNow....................................604 NSTermintateLater..................................604 -replyToApplicationShouldTerminate\.604 -requestUserAttention\...........................602 -runModalSession:...................................717 -sendEvent:...................................... 152, 154 -setApplication IconImage\.........................................590 -setLaunchFlags\......................................647 -stopModal................................................718 NSApplicationDelegate applicationDidBecomeActive\...............598 applicationDidResignActive\.................598 applicationShouldHandleReopen\ hasVisibleWindows\.........................599 -applicationShouldOpenUntitledFile\..601 -applicationShouldTerminate\...............604 applicationWillBecomeActive\..............598 applicationWillResignActive\................598 NSArray addObservers: toObjectsAtIndexes: forKeyPath:options:....................299 removeObservers: fromObjectsAtIndexes: forKeyPath:..................................298 NSArrayController -arrangedObjects.....................................327
925
SmartBooks
Objective-C und Cocoa – Band 2
NSAttributedString........................................544 -attributesAtIndex: effectiveRange:...................................548 longestEffectiveRange: inRange:.......................................548 NSBezierPath....................................................52 -appenBezierPathWithGlyph: inFont:...................................................67 -appendBezierPathWithArcFromPoint: toPoint: radius:.............................................62 -appendBezierPathWithOvalInRect:......60 -appendBezierPathWithRect:...................60 +bezierPath.................................................60 +bezierPathWithOvalInRect:...................60 +bezierPathWithRect:...............................60 +bezierPathWithRoundedRect: xRadius: yRadius:..........................................59 -closePath............................................. 53, 62 -curveToPoint: controlPoint1: controlPoint2:................................62 -fill................................................................59 -lineToPoint:...............................................61 -moveToPoint:............................................61 -setClip:.......................................................89 -setLineCapStyle:.......................................75 -setLineDash: count:....................................................75 -setLineWidth:............................................75 -stroke..........................................................59 NSBundle.........................................................628 -builtInPlugInsPath.................................627 +bundleWithPath\...................................628 -loadAndReturnError\............................628 +mainBundle............................................627 -principalClass.........................................629 NSCell..............................................................382 -alignment.................................................378 -backgroundStyle.....................................383 -continueTrackingAt: inView:................................................392 -drawInteriorWithFrame: inView:................................................385 -drawWithFrame: inView:................................................385 -highlightColorWithFrame: inView:................................................403
926
-highlighted..............................................403 -initTextCell:.............................................383 -initWithFrame........................................378 -startTrackingAt: inView:................................................392 -stoppTrackingAt: inView:................................................392 -trackMouse: inRect: ofView:untilMouseUp:...............392 NSCIImageRep...............................................450 +imageRepWithCIImage:.......................450 NSClassFromString().....................................889 NSCollectionView..........................................648 NSColor alternateSelectedControlColor...............403 -controlTextColor....................................404 secondarySelectedControlColor............403 -set................................................................44 -setFill..........................................................44 -setStroke....................................................44 NSConditionLock..........................................738 NSContainsRect()............................................43 NSControl -cell............................................................378 -cellSize......................................................381 -state..........................................................378 NSCopyObject().............................................411 NSCredentialStorage......................................696 NSDirectoryEnumerator...............................573 -fileAttributes...........................................574 -fileAttributes\..........................................573 -skipDescendants\...................................573 NSDivideRect().................................................42 NSDockTile display........................................................594 -setBadgeLabel\........................................590 -setContentView\.....................................594 NSDraggingDestination -concludeDragOperation:.......................202 -draggingEnded:......................................198 -draggingEntered:........................... 197, 202 -draggingExited:.......................................197 -performDragOperation:........................201 -prepareForDragOperation:...................201 NSDraggingInfo -draggingSourceOperationMask:..........197
Index
NSDraggingSource -draggedImage: beganAt:..............................................194 endedAt: operation:.....................................195 -draggingSourceOperation MaskForLocal:..........................................193 -ignoreModifierKeysWhileDragging....194 NSDrawBezel().................................................48 NSDrawButton()..............................................48 NSDrawGroove()..............................................48 NSDrawNinePartImage()................................47 NSDrawThreePartImage()..............................47 NSDrawTiledRects()........................................48 NSDrawWindowBackground()......................45 NSEntityMapping...........................................778 -mappingType..........................................778 -setAttributeMappings............................779 -setDestinationEntityName....................778 -setName...................................................778 -setRelationshipMappings......................779 -setSourceEntityName............................778 NSEntityMigrationPolicy..................... 780, 787 -createDestinationInstances ForSourceInstance\ entityMapping\ manager\error.............................785 -createRelationshipsFor DestinationInstance\ entityMapping\ manager\error\............................786 -setEntityMigrationPolicyClassName...780 NSEqualPoint().................................................43 NSEqualRect()..................................................43 NSEqualSize()...................................................43 NSEraseRect()...................................................45 NSError NSErrorFailingURLStringKey...............720 NSEvent -characters.................................................209 -keyCode...................................................209 location......................................................168 -modifier...................................................170 NSEvent: -currentEvent............................................400 NSException....................................................904 NSFileHandle..................................................577 -fileHandleForReadingAtPath\..............578 NSFileHandleNotificationDataItem......579
NSFileHandleReadCompletion Notification...................................... 580, 759 -readDataToEndOfFile\..........................578 -readInBackgroundAndNotify..............580 -readInBackGroundAndNotify.............579 NSFileManager -attributesOfItemAtPath\ error\...................................................576 -contentsOfDirectoryAtPath\ error\...................................................574 -copyItemAtPath\ toPath\ error\.............................................575 -createSymbolicLinkAtPath\ withDestinationPath\ error\.............................................575 +defaultManager............................. 571, 572 -displayNameAtPath\..............................576 -enumeratorAtPath\................................573 -linkItemAtPath\ toPath\ error\.............................................575 -moveItemAtPath\ toPath\ error\.............................................575 -removeItemAtPath\ error\...................................................575 -setAttributes\ ofItemAtPath\ error\.............................................576 -subpathsAtPath.......................................572 -subpathsAtPath\.....................................573 NSFileWrapper...............................................576 NSFont.............................................................517 -displayName............................................519 -fontFamilyName.....................................519 -fontName.................................................519 +fontWithName:........................................67 NSFontAction.................................................518 NSFontDescriptor..........................................517 NSFontFamilyClass........................................517 NSFontManager..............................................517 -selectedFont............................................519 +sharedFontManager..................... 518, 519 NSFrameRect…().............................................45 NSGlyph................................................... 68, 516
927
SmartBooks
Objective-C und Cocoa – Band 2
NSGradient.............................................. 48, 798 -drawInRect: angle:.....................................................49 relativeCenterPosition:.......................50 -initWithColors:.........................................49 andLocations:.......................................50 NSGraphicsContext................................ 85, 457 -currentContext.......................................137 +graphicsContextWithGraphicsPort: flipped:................................................495 -isDrawingToScreen................................137 +restoreGraphicsState...............................86 +saveGraphicsState....................................86 +setCurrentContext:................................495 NSHeight()........................................................40 NSHTTPURLResponse.................................661 -statusCode...............................................673 NSImage............................................. 78, 82, 647 -drawInRect: fromRect: operation:fraction:........................80 +imageNamed:...........................................80 +imageTypes...............................................79 -initWithSize:..............................................82 -lockFocus...................................................82 -lockFocusOnRepresentation:..................82 NSBitmapImageRep..................................79 NSCachedImageRep..................................79 NSCIImageRep...........................................79 NSCustomImageRep.................................79 NSEPSImageRep........................................79 NSPDFImageRep.......................................79 NSPICTImageRep.....................................79 -TIFFRepresentation........................ 82, 493 +unfilteredImageTypes.............................79 -unlockFocus..............................................82 NSImageView.................................................649 -setImageFrameStyle:..............................475 NSIndexSpecifier............................................638 NSInsetRect()....................................................41 NSInteger.........................................................831 NSIntegralRect()...............................................43 NSIntersectionRect().......................................43 NSInvocation -invokeWithTarget...................................889 NSKeyValueBindingCreation.......................302 bind: toObject: withKeyPath:options:........ 302, 307 unbind:......................................................302
928
NSKeyValueObserving addObserver: forKeyPath: options:context............................260 NSKeyValueChangeKindKey.................264 NSKeyValueChangeNewKey..................264 NSKeyValueChangeOldKey...................264 NSKeyValueObservingOptionNew......260, 263 NSKeyValueObservingOptionOld........263 NSKeyValueObservingOptions:............263 NSKeyValueOChangeNewKey:.............261 observeValueForKeyPath: ofObject: change:context:............................261 NSLocalDomainMask....................................570 NSLock.............................................................738 -lockBeforeDate:......................................739 -tryLock.....................................................739 NSMake….......................................................843 NSMakePoint().................................................40 NSMakeRect()...................................................40 NSMakeSize()...................................................40 NSManagedObjectModel..............................778 -isConfiguration\ compatibleWithStoreMetadata\......791 NSMappingModel..........................................778 -entityMappings.......................................778 +mappingModelFromBundles\ forSourceModel\ destinationModel\............. 778, 791 NSMaxX().........................................................40 NSMaxY()..........................................................40 NSMenu popUpContextMenu: withEvent: forView:........................................399 NSMiddleSpecifier.........................................638 NSMidX()..........................................................40 NSMidY()..........................................................40 NSMigrationManager........................... 780, 785 -addPersistentStoreWithType\ configuration\ URL\.............................................780 -associateSourceInstance\ withDestinationInstance\ forEntityMapping\......................786
Index
-destinationInstancesForEntityMappingNamed\ sourceInstances\................................787 -initWithSourceModel\ destinationModel\.............................791 -migrateStoreFromURL\ type\ options\withMappingModel\toDestinationURL\destinationType\ destinationOptions\error...........791 NSMinX()..........................................................40 NSMinY()..........................................................40 NSMutableAttributedString..........................544 NSMutableURLRequest.................................661 -addValue\ forHTTPHeaderField\......................661 -setHTTPBody\........................................685 -setHTTPMethod\...................................685 -setValue\ forHTTPHeaderField\......................661 NSNameSpecifier............................................638 NSNetworkDomainMask..............................570 NSNotFound...................................................843 NSObject -doesNotRecognizeSelector....................903 -forwardingTargetForSelector................902 -forwardInvocation..................................903 -ibPopulateAttributeInspectorClasses\.803 -ibPopulateKeyPaths\..............................803 -methodSignatureForSelector................903 -objectIdentifier.......................................637 -performSelector…..................................888 -performSelectorOnMainThread: withObject: waitUntilDone:............................729 -superclass.................................................901 NSOffsetRect()..................................................42 NSOpenPanel +openPanel...............................................586 -runModalForDirectory\ file\ types\............................................587 NSOperation...................................................740 NSOperationQueue.......................................740 -setMaxConcurrentOperationCount:...749
NSPageLayout.................................................134 beginSheetWithPrintInfo: modalForWindow: delegate:didEndSelector:contextInfo:...............................................134 -printInfo..................................................134 NSPasteboard -declareTypes: owner:.................................................188 -pasteBoardWithName:..........................188 -setData: forType:...............................................188 NSPersistentDocument -configurePersistentStoreCoordinator ForURL\ ofType\ modelConfiguration\ storeOptions\error......................781 NSPersistentStore...........................................780 NSPersistentStoreCoordinator.....................780 +metadataForPersistentStoreOfType\ URL\ error\.............................................791 NSPoint............................................................842 NSPointInRect()...............................................43 NSPrintInfo.....................................................133 -bottomMargin.........................................133 -leftMargin................................................133 NSLandscapeOrientation........................134 NSPorttraitOrientation...........................134 -paperSize.................................................133 -rightMargin.............................................133 -setOrientation:........................................133 +setSharedPrintInfo:...............................134 -topMargin................................................133 NSPrintOperation..........................................135 +printOperationWithView:....................136 -runOperation..........................................136 NSPropertyDescription -validationPredicates\..............................786 NSPropertyMapping......................................779 -name\.......................................................786 -setName\..................................................780 -setValueExpression\...............................780 -valueExpression\....................................786 NSPropertySpecifier.......................................638 NSRandomSpecifier.......................................638 NSRange..........................................................842 NSRangeSpecifier...........................................638
929
SmartBooks
Objective-C und Cocoa – Band 2
NSRect.............................................................842 NSRectFill…()..................................................45 NSRecursiveLock...........................................738 NSRelativeSpecifier........................................638 NSResponder........................................... 16, 157 -flagesChanged:........................................212 -keyDown:........................................ 209, 422 -mouseDown:...........................................164 -mouseDrag:.............................................172 -mouseEntered:........................................205 -mouseExited:...........................................205 -mouseMoved:..........................................206 -mouseUp:................................................164 -noResponderFor:....................................157 NSRunLoop addTimer: forMode:.............................................714 +currentRunLoop....................................714 DefaultRunLoopMode............................707 EventTrackingRunLoopMode................707 ModalPanelRunLoopMode....................707 +scheduledTimerWithTimeInterval: target: selector:userInfo:repeats:...........711 NSScreen...........................................................84 +mainScreen...............................................84 NSScriptObjectSpecifier................................638 NSSearchPathForDirectoriesInDomains().570 NSSetFocusRingStyle.......................................89 NSSharedWorkspace duplicateURLs\ completionHandler\..........................588 NSSize..............................................................842 NSStatusBar NSVariableStatusItemLength.................606 -removeStatusItem\.................................607 -statusItemWithLength\..........................606 +systemStatusBar.....................................606 NSStatusItem NSSquareStatusItemLength....................610 -popUpStatusItemMenu\........................612 -setHighlightedMode\.............................607 -setMenu\..................................................606 -setTitle\....................................................606 -setView\...................................................610 NSString -rangeOfComposedCharacterSequencesForRange:..................................................514
930
-stringByAddingPercentEscapesUsing Encoding\..................................................684 +stringWithContentsOfURL\................660 NSStringDrawing -sizeWithAttributes\................................592 NSSystemDomainMask.................................570 NSTableView -headerCell...............................................398 -highlightColorWithFrame: inView:................................................403 -selectionHighlightStyle..........................404 tableColumns...........................................398 tableColumnWithIdentifier:...................398 NSTask -launch.......................................................754 +launchedTaskWithLaunchPath: arguments:..........................................756 +pipe..........................................................756 -setArguments:.........................................755 -setCurrentDirectoryPath:......................755 -setLaunchPath:.......................................754 -setStandardOutput:................................756 -standardOutput......................................757 NSText -setFieldEditor:.........................................434 NSTextContainer............................................534 -setHeightTracksTextView:....................530 -setWidthTracksTextView:.....................530 NSTextStorage.................................................544 -attributeRuns...........................................549 NSTextView -selectedRanges........................................532 -setHorizontalyResizable:.......................531 -setVerticallyResizable:...........................531 NSThread.........................................................721 -cancel:......................................................725 +detachNewThreadSelector: toTarget: withObject:..................................723 -initWithTarget: selector: object:...........................................724 -isCancelled..............................................725 -isExecuting..............................................726 isFinished..................................................726 +sleepUntilDate:......................................723 -start...........................................................724 NSTimeInterval..............................................711
Index
NSTimer..........................................................708 -invalidate.................................................712 +timerWithTimeInterval: target: selector:userInfo:repeats:...........714 NSTypeSetter -lineFragmentRectForProposedRect: sweepDirection: movementDirection:remainingRect:..............................................535 NSUInteger......................................................831 NSUnionRect().................................................43 NSUniqueIdentifier........................................638 NSURLConnection............................... 661, 662 -connection\ didFailWithError\.............................676 didReceiveResponse\........................672 -connectionDidFinishLoading\.............675 +sendSynchronousRequest\ returningResponse\ error\.............................................662 NSURLCredential..........................................696 NSURLDownload..........................................719 -initWithRequest: delegate:..............................................719 -setDestination: allowOverwrite:.................................720 NSURLRequest...............................................661 NSURLResponse............................................661 NSUserDomainMask.....................................570 NSView -acceptsFirstMouse..................................162 -acceptsFirstResponder.................. 206, 209 -becomeFirstResponder..........................209 -bounds.......................................................33 -centerScanRect:......................................114 -convertPoint: fromView:.................................. 165, 195 -convertPointToBase:..............................117 -convertRectFromBase:...........................117 -convertRectToBase:................................117 -convertSizeToBase:.................................117 -dragImage:at: offset:event: pasteboard:source:slideBack:.....189 -drawRect:...................................................16 -enterFullScreenMode: withOptions:........................................84
-exitFullScreenMode: withOptions:........................................84 -frame..........................................................33 -getRectsBeingDrawn: count:..................................................130 -isFlipped....................................................35 -knowsPageRange....................................142 -needsToDrawRect:.................................131 -rectForPage..............................................142 -resetCursorRects:...................................202 -resignFirstResponder.............................209 -setAcceptsMouseMoveEvents:..............206 -setAlphaValue:........................................469 -setAutoresizingMask:.............................475 -setBackgroundFilters:............................469 -setBounds..................................................33 -setBoundsRotation...................................33 -setCompositingFilter:............................469 -setFrameOrigin:......................................466 -setNeedsDisplay:..................... 17, 124, 183 -setNeedsDisplayInRect:.................. 17, 126 -setWantsLayer:........................................469 -wantsDefaultClipping:...........................128 NSViewController -initWithNibName\ bundle\................................................629 NSWhoseSpecifier..........................................638 NSWidth().........................................................40 NSWindow -convertPointFromBase:.........................195 -dockTile...................................................589 -initWithContentRect: styleMask: backing:defer:..............................427 -invalidateCursorRectsForView:...........203 -nextEventMatchingMask:......................182 -sendEvent:...................................... 152, 155 -setAlphaValue:........................................428 NSWorkspace -duplicateURLs\ completionHandler\..........................586 -notificationCenter..................................584 -performFileOperation\ source\ destination\files\tag....................588 +sharedWorkspace..................................584 NSXMLDocument.........................................682 NSXMLNode..................................................680 NSZero….........................................................843
931
SmartBooks
Objective-C und Cocoa – Band 2
off_t..................................................................831 OSAScriptingDefinition................................632 Protocol............................................................838 QTMovieLayer................................................469 random().........................................................227 readonly...........................................................871 readwrite..........................................................871 register.............................................................854 retain................................................................872 return...............................................................885 SBApplication..................................................642 -activate.....................................................643 +applicationWithBundleIdentifier\.......643 -classForScriptingClass...........................656 -isRunning................................................643 isRunning..................................................647 SBElementArray.............................................642 -addObject\...............................................658 -get.............................................................646 -objectWithName\...................................644 SBObject................................................. 642, 644 -exists\.......................................................644 -get.............................................................646 -init............................................................657 -initWithProperties\................................657 Scripting sdef.............................................................632 SEL....................................................................840 setter=..............................................................873 short.................................................................830 signed...............................................................831 sizeof().............................................................863 size_t....................................................... 830, 831 srandom()........................................................227 static (globale Variable).................................852 static (lokale Variable)...................................850 struct................................................................842 super.................................................................899 switch...............................................................882 typedef.............................................................847 unichar.................................................... 513, 831 union................................................................843 unsigned..........................................................830 while (do)........................................................883 while (while)...................................................882 YES...................................................................831
932
Stichwortverzeichnis A Action...............................................................215 Applikation...................................... 215, 218 Dispatch....................................................215 Key-Window.............................................217 Main-Window..........................................217 vordefinierte Methoden..........................215 Adressoperator................................................862 Aktivierung (Applikation) anfordern..................................................601 Anforderungs-ID.....................................603 Delegatemethode.....................................598 Dock..........................................................598 Dokument.................................................599 Hüpfendes Dock-Icon.................... 598, 601 Strategie.....................................................598 Alias. Siehe Finder-Link Antialiasing.......................................................21 ausschalten..................................................24 AppKit, Threadfestigkeit................................727 AppleScript Siehe auch Scripting......... 631, 634 AppleScript-Editor.........................................631 Application Support-Verzeichnis.................566 Applications-Verzeichnis...............................566 Applikation Aktivierungsverhalten.............................589 Klasse.........................................................156 Menü ausblenden.....................................612 Terminierung............................................604 Architekturmodell..........................................830 Ganzzahltypen..........................................834 ILP32.........................................................833 LP64...........................................................833 Array-Controller, ableiten.............................327 ASCII...............................................................511 Assoziation (Laufzeitsystem)........................917 ATSUI..............................................................508 Aufzählungsoperator.....................................863 Aufzählungstyp...............................................841 Ausdruck.........................................................856 Bit-Operatoren.........................................859 Mengen als Bitsets....................................861 Ausführungseinheit........................................879 Anweisung................................................880
Index
Ausführungsoperator.....................................865 Autoreleasepool, Thread................................722 Autovariable....................................................850
B Badge................................................................401 Bedingte Kompilierung.................................765 Benutzerverzeichnis.......................................566 Berechnete Eigenschaft, Implementierung mittels Getter...................................................336 Bezier-Kurve.....................................................54 graphische Eigenschaften..........................55 Bezierpfad hohl............................................................592 Winding-Rule...........................................592 Bezier-Pfad........................................................52 Attribute......................................................72 Ausgabe von Standardformen..................57 Default-Attribute.......................................73 freier Subpfad.............................................61 Füllregel.......................................................76 gemischte Attribute...................................72 Glyph...........................................................66 Kreissegment..............................................63 Linie.............................................................63 Linienstärke und Formen.........................73 Segmente.....................................................52 Subpfad........................................................52 Subpfad schließen......................................53 Transformation..........................................96 Vorgefertigte Formen................................58 Bildschirmausgabe...........................................14 Binding Aufgabe.....................................................254 Rolle...........................................................254 Bindings.................................................. 252, 301 abmelden...................................................312 berechnete Eigenschaften.......................334 Controller-Key.........................................307 Dynamisch verwalten..............................314 Eigenschaft................................................302 einrichten..................................................307 Mehrfachbindings....................................314 Model-Key-Path.......................................307 Option.......................................................318 Schlüsselpfad verschmelzen...................307 Threadfestigkeit........................................727 Bindings-Controller.......................................323
Block (Objective-C).......................................885 Ausführung...............................................886 Block-Variable..........................................887 Closure......................................................886 Lebensdauer..............................................888 Literal.........................................................885 Objektvariablen........................................887 Referenz.....................................................885 Returnwert................................................885 Speicherverwaltung.................................887 Variable......................................................886 Blockreferenzen..............................................840 Blocks...............................................................587 Blockvariablen................................................855 boolscher Ausdruck.......................................858 und BOOL................................................858 und nil, Nil................................................859 boolscher Operator........................................858 Bounds...............................................................24 Änderung....................................................29 Build-Konfiguration, für Softwareversion..772 Bundle, lokalisierter Name............................569 by-Value, Funktion.........................................885
C Cap-Height......................................................525 C-Array............................................................844 als Parameter............................................845 mehrdimensional.....................................844 Zuweisung.................................................845 Zuweisung an Zeiger...............................845 C-Block............................................................880 externe Variablen.....................................880 lokale Variablen........................................880 Cell...................................................................382 Auswertung von Attributen....................384 Eigenschaft für Control...........................378 Event................................................. 390, 420 Größenberechnung..................................381 kombinieren.............................................409 kopieren.....................................................411 Mouse-Tracking.......................................392 Stempelkonzept........................................376 Text ausgeben...........................................390 und Control..............................................376 und Tableview..........................................398 verwenden.................................................383 zeichnen....................................................384
933
SmartBooks
Objective-C und Cocoa – Band 2
CGImage..........................................................493 Character. Siehe Zeichen CICategoryCompositeOperation.................452 CILanczosScaleTransform............................450 Class-Continuation, Eigenschaft..................872 Clipping.............................................................88 C-Objekt..........................................................143 erzeugen....................................................146 Freigabe.....................................................146 Objekt........................................................144 Referenz.....................................................145 Speicherverwaltung.................................145 Struktur (C)..............................................143 Umwandlung............................................146 Cocoa, Threadfestigkeit.................................727 Cocoa Bundle......................................... 615, 618 Concurrent Versions System.........................808 Control.............................................................376 ableiten......................................................377 Bezeled......................................................378 Eigenschaft................................................378 Event................................................. 390, 420 Rückweg zum Controller........................422 und Cell.....................................................376 Core Animation..................................... 438, 463 Animatable-PropertyContainer-Protokoll................................466 Animationsklassen...................................467 Animator...................................................466 Animieren einer Eigenschaft..................463 Layer im dreidimensionalem.................502 Layerklassen.............................................468 Sublayer.....................................................492 Transformationen....................................502 Core Data Änderung des Models.............................775 Attributemappings...................................779 Datenmigration........................................775 Mappingmodel.........................................777 Model Version..........................................776 Relationshipmappings.............................779 Threadfestigkeit........................................731 versioniertes Model.................................776 Core Foundation............................................143 Core Graphics.................................................143 Core Image......................................................438 Checkerboard-Generator........................452 CIAffineTile..............................................459 CICategoryBlur........................................439
934
CICategoryColorAdjustment.................439 CICategoryColorEffect............................439 CICategoryCompositeOperation..........439 CICategoryDistortionEffect...................439 CICategoryGenerator..............................439 CICategoryGeometryAdjustment.........439 CICategoryGradient................................439 CICategoryHalftoneEffect......................439 CICategoryReduction.............................439 CICategorySharpen.................................439 CICategoryStylize....................................439 CICategoryTileEffect...............................439 CICategoryTransition..............................439 CICheckerboardGenerator.....................454 CIColorControls......................................441 CIDotScreen.............................................459 CISourceOverCompositing....................452 CISunbeamsGenerator............................455 Filter...........................................................438 Filter Reference........................................440 Fun House.................................................440 Helligkeit...................................................443 Konstrast...................................................443 Lazy-Evaluation.......................................450 Quartz Core..............................................443 Sättigung...................................................443 Core Text.........................................................508 Cursor-Rects...................................................202 CVS...................................................................808
D Data-Source, Typumwandlung.....................250 Datei.................................................................555 Dateiattribut....................................................576 Dateilink..........................................................559 Dateiname.......................................................555 Dateinamen.....................................................557 Dateiordner. Siehe Dateiverzeichnis Dateipfad.........................................................569 Dateisystem.....................................................554 Lokalisierung............................................568 Nummer....................................................556 Reference-Counting................................560 Verzeichnisbaum......................................565 Dateisystemknoten.........................................555 Nummer....................................................555 Dateiverzeichnis.................................... 555, 557 lesen...........................................................572
Index
Datenfluss........................................................230 Datentyp boolscher...................................................831 Casting.......................................................855 einfacher....................................................830 Define...............................................................763 Definition in der Source..........................763 Funktionsweise.........................................763 Leere Definition.......................................765 Problematik..............................................764 Dekrementierung...........................................857 Delegating........................................................349 anbieten.....................................................354 Beziehung..................................................353 Methodennamen......................................350 Struktur der API......................................350 und Subklassen.........................................349 Dereferenzierungsoperator...........................862 Designated-Initializer....................................911 Display-Name.................................................569 Dock.................................................................589 Badge................................................ 589, 590 Icon ausblenden.......................................612 Image.........................................................590 Tile.............................................................589 View...........................................................590 Dockmenü.......................................................596 First-Responder.......................................597 laden..........................................................597 Menü-Nib-Datei.......................................598 Nib-Datei..................................................597 Domain (Dateisystem)...................................567 Local..........................................................567 Network.....................................................567 Shared........................................................567 System........................................................567 User............................................................567 Dot-Notation, Explizite Accessormethode.873 Drag..................................................................171 Bedeutung.................................................172 Implementierungsvarianten...................175 modal.........................................................182 unmodal....................................................177 Drag & Drop............................................ 14, 185 Draginformation......................................198 Modifier.....................................................199 MVC-Muster............................................185 Operation ausführen...............................201 Pasteboard.................................................188
Quelle........................................................192 Registrierung............................................196 Senke..........................................................196 starten............................................... 187, 189 Drucken...........................................................132 ausführen..................................................135 Beteiligte Objekte.....................................132 Paginierung...............................................138 Standardimplementierung......................136 DS_Store..........................................................558 DTrace..............................................................821
E Echtzeit............................................................438 Eigenschaft abweichende Instanzvariable..................876 Accessoroption.........................................873 Atomar.......................................................873 automatische Instanzvariable.................876 Definition..................................................875 Dynamische..............................................876 einfacher Typ............................................872 implitize Instanzvariable.........................876 Kategorie...................................................877 Speicherverwaltung Getter.....................874 Speicherverwaltungsoption....................872 Speicherverwaltungsoptionen Semantik...................................................872 Wechsel der Zugriffsoption....................872 Zugriffsoptionen......................................871 Event.................................................................150 AppKit-Defined........................................156 Dispatch........................................... 150, 152 Eventmethode..........................................152 Mausbewegung.........................................206 Modale Abfrage........................................182 Ohne Abnehmer......................................157 System-Defined........................................156 Time-Stamp..............................................151 Zeichnen...................................................183 Zuordnung zu Objekt..............................150 Exception.........................................................903 erneut werfen............................................907 fangen........................................................905 finally-Block..............................................907 try-Block...................................................906 werfen........................................................904 Externe Variable.............................................853 Extra-Bytes......................................................916
935
SmartBooks
Objective-C und Cocoa – Band 2
F Farbe, setzen......................................................44 Farbverlauf. Siehe Gradient Faulheit, späte Initialisierung........................913 Fenster ableiten......................................................424 Content-View...........................................431 Designated-Initializer..............................427 Event-Dispatch.........................................152 Key-Window.............................................217 konfigurieren............................................428 Main-Window..........................................217 Transparenz..............................................428 Field-Editor.....................................................432 anpassen....................................................433 Spezialtasten.............................................434 und Responder-Chain.............................432 File-Handle asynchrone Operation.............................579 synchrone Operation...............................577 File-Handles....................................................577 Filemanager.....................................................571 File-Manager Dateioperationen.....................................575 Verzeichnisoperationen..........................575 File-Wrapper...................................................576 Finder-Links....................................................563 First-Responder, Abfrage..............................405 Fließkommazahl.............................................836 dezimal......................................................836 Exponent...................................................837 literale Konstante.....................................836 Mantisse....................................................837 Font................................................... 88, 510, 516 Bemaßung.................................................520 Beschreibung............................................517 Bezeichnungen.........................................519 Family-Class.............................................517 FontTrait....................................................517 kursiv usw..................................................517 Font-Descriptor..............................................517 Font-Family-Class..........................................517 Fontsystem............................................. 509, 510 Begrifflichkeiten.......................................510 Fontverwaltung...............................................517 Actionmethoden......................................518 Fontpanel..................................................517 Menü..........................................................517
936
Singleton...................................................518 Frame.................................................................24 Framework......................................................791 Funktion..........................................................884 Definition..................................................884 Deklaration...............................................884 Parameter..................................................884 return.........................................................885 Funktionsausdruck.........................................786 Funktionszeiger..............................................839
G Ganzzahl..........................................................830 literale Konstante.....................................831 Wertebereich.............................................832 Gleitkommazahl. Siehe Fließkommazahl Glyph......................................................... 66, 516 Generator..................................................527 GPU..................................................................452 Gradient.............................................................48 linear............................................................48 radial............................................................48 Graphischer Kontext........................................85 Attribute......................................................86 Aufgabe.......................................................85 Transformation..........................................89 Größe (Graphik) erzeugen......................................................40 vergleichen..................................................43 Größenoperator..............................................863 Grundrechenarten..........................................857
H Hard-Link........................................................559 HFS+................................................................554
I Image Ausgabe.......................................................78 Repräsentation...........................................78 zeichnen......................................................82 Info.plist, AppleDockMenu...........................598 Inkrementierung............................................857 Inode. Siehe Dateisystemknoten Instanzvariable Definition..................................................868
Index
Kategorie...................................................869 Name.........................................................868 Qualifizierer..............................................868 Sichtbarkeit...............................................868 Instruments.....................................................821 Leak............................................................823 Interface Builder Plug-in...............................802
K Kategorie Eigenschaft................................................877 Erstellen.....................................................291 Key-Value-Observing....................................252 Änderung bei To-Many feststellen........275 Arten..........................................................256 Attribut-Aggregation...............................296 Aufgabe.....................................................254 Bindings....................................................301 Doppelte Mitteilung................................264 Einrichten.................................................260 einzelnes Attribut.....................................258 Entfernen von Elementen zu einer Collection..................................................298 Hinzufügen von Elementen zu einer Collection..................................................299 Initiale Mitteilung....................................264 Observierung durchführen....................261 Option.......................................................263 Probleme der Kapselung.........................301 Relation mit Set........................................271 Rolle...........................................................254 Schlüsselpfade..........................................266 To-Many mit Array..................................282 Klammeroperator...........................................865 Klasse Basisklasse.................................................868 Class-Continuation..................................874 Deklaration...............................................867 Forward-Declaration...............................874 Implementierung.....................................875 Methodendefinition.................................875 Name.........................................................867 Protokollliste.............................................868 Klassenobjekt, Erzeugung.............................908 Klassenobjektzeiger........................................838 Kontext (Graphik). Siehe Graphischer Kontext Kontextmenü, öffnen.....................................400
Kontrollfluss....................................................231 Linearität...................................................698 Kontrollstruktur.............................................880 bedingte Anweisung................................881 do-Wiederholung.....................................883 for-in-Wiederholung...............................884 for-Wiederholung....................................883 if-Kaskade.................................................881 Mehrfachauswahl.....................................882 Unterbrechung................................ 882, 884 Vorzeitige Wiederholung........................884 while-Wiederholung................................882 Koordinatensystem..........................................19 Bezug...........................................................24 Drehung......................................................33 Fließkommazahlen....................................19 Maßstab.......................................................19 Spiegelung...................................................35 Vergrößerung.............................................30 Verschiebung..............................................29
L Laufzeitmaschine, Featureset........................830 Laufzeitsystem................................................914 Ermittlung des Klassenobjektes.............917 Instanzerzeugung.....................................916 Instanzmethode........................................918 Instanzvariable.........................................916 Klasseninformation.................................915 Klassenobjekt............................................917 Klassensystem..........................................918 Metaklassenobjekt...................................918 Nachricht..................................................920 Protokoll....................................................920 Layoutstapel....................................................526 erstellen.....................................................528 Glpyhgenerator........................................527 Layoutmanager.........................................527 Speicherverwaltung.................................528 Textcontainer............................................528 Textstorage................................................527 Textview....................................................528 Typesetter..................................................527 Layoutsystem......................................... 509, 525 Library-Verzeichnis........................................566 Ligature, Unicode...........................................515 Line-Fragment-Padding................................540
937
SmartBooks
Objective-C und Cocoa – Band 2
Linie Attribute......................................................22 Form..................................................... 23, 73 Muster.........................................................75 Stärke.................................................... 22, 73 literale Konstante Block..........................................................840 Fließkomazahl..........................................836 Ganzzahl...................................................831 lvalue................................................................864
M Mapping, Custom-Policy...............................785 Mappingmode, konfgurieren........................777 Mausevent Dispatch....................................................161 Inaktives Fenster......................................161 Location....................................................165 Methoden..................................................163 Modifier.....................................................169 Mausklick........................................................150 Mauszeiger anpassen......................................202 Metaklassenobjekt..........................................918 Methode Klassenmethoden und Instanzmethoden.....................................870 Name.........................................................870 Parameter..................................................871 Rückgabetyp.............................................870 Signatur.....................................................889 Überladen.................................................870 Methodenzeiger..............................................839 MLTE...............................................................508 Mount...............................................................565
N Nachricht.........................................................888 Adressat.....................................................889 an Instanz..................................................892 an Klasse....................................................897 an Klassenobjekt......................................893 an self.........................................................894 an super.....................................................899 an Superklasse..........................................900 Bestandteile...............................................889 Dispatching...............................................891 Dynamische Bindung..............................891
938
Forward-Invocation.................................903 Forward-Target........................................902 Gescheiteter Dispatch..............................902 Laufzeitsystem..........................................920 Parameter..................................................890 Selektor......................................................889 Signatur.....................................................889 Statische Bindung....................................897 versenden..................................................888 Nebenläufigkeit...............................................698 Arten der Implementierung...................701 Geteilte Ressourcen.................................700 Kommunikation.......................................700 Netzwerk...................................................718 Network-Verzeichnis.....................................566 NSAddEntityMappingType...........................779 NSApplicationSupportDirectory..................570 NSBitmapImageRep.......................................493 NSCacheDirectory.........................................570 NSCopyEntityMappingType.........................779 NSCustomEntityMappingType....................779 NSDocumentDirectory.................................570 NSDownloadsDirectory................................570 NSLibraryDirectory.......................................570 NSRemoveEntityMappingType....................779 NSTransformEntityMappingType................779 NSUndefinedEntityMappingType................779 NSURL.............................................................659 Nutzereingabe.................................................150
O Objective-C, Version......................................830 Objekterzeugung automatische Initialisierung...................910 Initialisierungsstrategie...........................912 Instanzobjekt............................................909 Klassenobjekt............................................908 Laufzeitsystem..........................................916 Reihenfolge der Initialisierung...............910 sofortige Initialisierung...........................912 späte Initialisierung.................................913 Objektzeiger....................................................838 Objektzerstörung............................................913 Garbage-Collection.................................914 Reference-Counting................................914 OpenGL...........................................................462
Index
Operation-Queue...........................................740 Operation-Arbitrierung..........................741 Operation-Subklasse...............................743 Operation (Thread). Siehe Operation-Queue Operatorpriorität............................................865 Ordner. Siehe Dateiverzeichnis
P Page-Layout. Siehe Seitenlayout Perforce............................................................808 Plugin...............................................................613 Cocoa Bundle...........................................615 Cocoa-Plugin............................................614 Interface....................................................620 Principal class...........................................615 Verzeichnis................................................623 Präprozessor Ausdruck...................................................769 Bedingte Kompilierung...........................762 Define........................................................762 Softwareversion........................................762 Vergleich...................................................769 Präprozessoranweisung, Syntax....................762 Protokoll..........................................................877 Datentypen...............................................242 Definition..................................................877 Einhaltung spezifizieren..........................238 Forward-Declaration...............................878 Implementierungsanforderung..............878 Laufzeitsystem..........................................920 Methode....................................................878 Name.........................................................878 Property.....................................................878 Typ.............................................................879 Zur Laufzeit..............................................879 Protokollzeiger................................................838 Prozessorarchitektur......................................762 Punktdatei.......................................................558 Punkt (Graphik) erzeugen......................................................40 vergleichen..................................................43
Q Qualifizierer....................................................849
R Rechteck Änderung....................................................41 erzeugen......................................................40 Funktionen.................................................39 vergleichen..................................................43 Zugriff..........................................................40 Reference-Counting.......................................914 Registervariable..............................................854 Repository-Configuration.............................810 Responder........................................................150 Subklassen.................................................152 Responder-Chain.................................. 150, 157 Next-Responder.......................................157 REST.................................................................662 Run-Loop Common-Mode.......................................708 Input-Sources...........................................706 Modal-Sessions........................................714 Mode..........................................................707 Nebenläufigkeit........................................706 Netzwerk...................................................718 Thread........................................................706 Run-Loop-Nebenläufigkeit...........................701
S Satusmenü.......................................................605 Schatten.............................................................88 Schriftschnitt. Siehe Font Scripting AppleScript freigeben..............................632 OSA-Scripting-Definition.......................632 Scripting-Bridge.......................................642 sdef.............................................................643 sdp..............................................................643 Standardsuite................................... 632, 634 XInclude....................................................632 Scripting-Bridge-Framework........................641 Scrolling.............................................................37 Secondary-Initializer.....................................911 Seitenlayout Querformat...............................................133 Singleton holen.........................................133 Systemdialog.............................................134 verändern..................................................133 Selektor............................................................840
939
SmartBooks
Objective-C und Cocoa – Band 2
Softwareversion Ausbaustufe..............................................762 Konfiguration mit dem Präprozessor....762 Prozessorarchitektur................................762 Sourcekontrollsystem. Siehe Versionskontrollsystem Sourceview, Selektionsfarben........................402 Speicherklasse.................................................849 Standardverzeichnisse....................................566 statische globale Variable...............................852 statische lokale Variable.................................850 Statusmenü......................................................589 Eintrag erzeugen......................................607 Menü öffnen.............................................611 quadratisch...............................................610 View...........................................................607 Struktur (C), OOP..........................................143 Strukturtyp......................................................842 Initialisierung...........................................842 Komponentenzugriff...............................843 Sublayer............................................................497 Subversion.......................................................808 Supplementary-Specialpurpose-Plane........511 SVN..................................................................808 svnadmin.........................................................809 Symbolic-Link.................................................561 Systemverzeichnis...........................................566
T Tablecolumn....................................................398 Tableview Selektionsfarben.......................................402 und Cell.....................................................398 Target-Konfiguration für Softwareversion..773 Task...................................................................754 Arbeitsverzeichnis...................................754 Argumente................................................754 ausführen..................................................754 Ausgabe asynchron empfangen.............758 erzeugen....................................................754 File-Handle...............................................756 Launchpfad...............................................754 Pipe............................................................756 Tastaturevent...................................................207 Dispatch....................................................208 Methoden..................................................209 Modifier.....................................................212 Weiterleitung............................................211
940
Testfall..............................................................825 Textattribut......................................................545 Absatz........................................................545 Arten..........................................................545 Attachments..............................................545 Bezeichner................................................546 Effektive-Range........................................548 Eigenes Attribut.......................................551 Longest-Effective-Range.........................548 setzen.........................................................549 Zeichen......................................................545 Textcontainer..................................................534 Textstorage......................................................544 Textsystem, Bereiche......................................508 Textview...........................................................532 Selektion....................................................532 Thread..............................................................721 AppKit.......................................................727 atomare Operation...................................733 automatische Erzeugung.........................722 Autoreleasepool........................................722 Bindings....................................................727 Core Data..................................................731 erzeugen....................................................722 Geteilte Ressource....................................732 Locking......................................................732 Locking (Cocoa)......................................738 Locking (Objective-C)............................735 Main-Thread.............................................704 Manuelle Erzeugung................................724 Methodendurchlauf abwarten................730 Mutex.........................................................735 Race-Condition........................................732 Run-Loop..................................................706 Synchronisierung.....................................732 wechseln....................................................729 Timer auszuführende Methode.........................712 erzeugen....................................................711 Run-Loop-Mode......................................713 Toll-Free-Bridging..........................................147 Tracking-Area.................................................203 Transformation Bezier-Pfad.................................................96 Darstellung..................................................93 Grundlagen.................................................91 Verkettung..................................... 93, 94, 95 Typdefinition...................................................847
Index
U Unicode............................................................511 ASCII-Kompatibilität..............................511 Basic-Multilinguale-Plane......................511 Codepoint.................................................514 Ebenen.......................................................511 Ligature.....................................................515 Multiword........................................ 513, 514 Normalisierung........................................515 Private-use-Area......................................511 Sortierung.................................................515 Supplementary-Ideographic-Plane........511 Supplementary-Multilinguale-Plane.....511 Tertiary-Ideographic-Plane....................511 UTF-8........................................................512 UTF-16......................................................512 Unionstyp........................................................843 Untypisierte Zeiger.........................................840 usr-Verzeichnis...............................................566
V Value-Transformer.........................................318 eigener.......................................................318 registrieren................................................320 Variable Extent................................850, 852, 853, 854 Scope..........................................................850 Variablenkonstante.........................................849 Vergleich..........................................................858 Fließkommazahl......................................858 Versionskontrollsystem.................................807 Änderung der Projektstruktur...... 813, 818 Checkout...................................................813 Client-Server-Architektur.......................807 Commit............................................ 813, 815 Kollision........................................... 813, 816 Konzept.....................................................807 lokal arbeiten............................................813 lokale Arbeitskopie erzeugen.................813 Lokalisierung hinzufügen.......................819 neue Klasse hinzufügen..........................818 Projekt.......................................................809 Projekt mit Repository verbinden.........811 Repository.................................................809 Repository als Datenbank.......................810 Repository anlegen..................................809 Repository in Xcode................................810
Rollback.....................................................819 und Xcode.................................................808 Update.............................................. 813, 816 Verzeichnisdatei..............................................557 View Eigenes Sizing...........................................110 Erzeugung im Sourcecode......................108 Frame ändern.............................................36 Hierarchie........................................ 107, 231 Volumes-Verzeichnis.....................................566 Vorzeichen.......................................................857
W Workspace.......................................................581 Dateioperationen............................ 586, 588 Notification-Center.................................581 WYSIWYG......................................................508
X Xcode Define........................................................770 Softwareversion........................................769 x-Height...........................................................525
Z Zeichen............................................................510 Ausgabe.....................................................520 Information und Darstellung.................510 Zeichenfunktionen...........................................45 Zeichenkodierung..........................................510 Zeichnen Anforderung zum Neuzeichnen............119 Farbe............................................................44 Fullscreen-Modus......................................83 Neuzeichnen...............................................16 pixelgenau.................................................113 Strategien....................................................18 Turtle-Graphik...........................................52 Zeiger...............................................................837 Zufallsgenerator..............................................227 Zuweisungsoperator.......................................864
941
eload 24
Viel guter Rat ab 3 Euro monatlich Die neuen Flatrate Modelle von eload24
Das ist ein Wort: Sie bekommen freien Zugang zu allen eBooklets und eBooks bei eload24. Sie können alles laden, lesen, ausdrucken, ganz wie es Ihnen beliebt. Eine echte Flatrate eben, ohne Wenn und Aber. Sie werden staunen: Unser Programm mit nützlichen eBooklet-Ratgebern ist groß und wird laufend erweitert. Der Preisvorteil ist enorm: 24 Monate Flatrate für nur 72,– E (3,– E monatlich) 12 Monate Flatrate für nur 48,– E (4,– E monatlich) 6 Monate Flatrate für nur 36,– E (6,– E monatlich) Selbst wenn Sie nur zwei eBooklets der preiswertesten Kategorie im Monat laden, sparen Sie im Vergleich zum Einzelkauf. Tausende Kunden haben dieses Angebot schon wahrgenommen, profitieren auch Sie dauerhaft. Wenn Sie nach Ablauf des Flatrate weitermachen wollen, dann brauchen Sie nichts zu tun: das FlatrateAbonnement verlängert sich automatisch. Bis Sie es beenden. Kaufen Sie jetzt die Flatrate Ihrer Wahl. Und schon einige Augenblicke später stehen Ihnen hunderte toller Ratgeber uneingeschränkt zur Verfügung: Packen Sie mal richtig zu!
www.eload24.com/flatrate eload24_Anzeige.indd 1
15.05.2008 12:51:15 Uhr
ratschlag24.com Das neue Ratgeber-Portal ratschlag24.com liefert Ihnen täglich die besten Ratschläge direkt auf Ihren PC. Viele bekannte Autoren, Fachredakteure und Experten schreiben täglich zu Themen, die Sie wirklich interessieren und für Sie einen echten Nutzen bieten. Zu den Themen zählen Computer, Software, Internet, Gesundheit und Medizin, Finanzen, Ernährung, Lebenshilfe, Lernen und Weiterbildung, Reisen, Verbrauchertipps und viele mehr. Alle diese Ratschläge sind für Sie garantiert kostenlos. Testen Sie jetzt ratschlag24.com – Auf diese Ratschläge möchten Sie nie wieder verzichten. ratschlag24.com ist ein kostenloser Ratgeber-Dienst der eload24 AG – www.eload24.com