Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Geleitwort
Vorwort
1 Hello iPhone
2 Die Reise nach iOS
3 Sehen und anfassen
4 Alles unter Kontrolle
5 Daten, Tabellen und Controller
6 Models, Layer, Animationen
7 Programmieren, aber sicher
8 Datenserialisierung und Internetzugriff
9 Multimedia
10 Jahrmarkt der Nützlichkeiten
Stichwort

Buch bestellen
Ihre Meinung?

Spacer
Apps programmieren für iPhone und iPad von Klaus M. Rodewig, Clemens Wagner
Das umfassende Handbuch
Buch: Apps programmieren für iPhone und iPad

Apps programmieren für iPhone und iPad
Rheinwerk Computing
1172 S., geb., mit DVD
49,90 Euro, ISBN 978-3-8362-2734-6
Pfeil 3 Sehen und anfassen
Pfeil 3.1 Eigene Viewklassen in Cocoa Touch
Pfeil 3.1.1 Zeichnen in Cocoa Touch
Pfeil 3.1.2 Zeitberechnung
Pfeil 3.1.3 View-Erzeugung über Storyboards
Pfeil 3.1.4 Aktualisierung der Zeitanzeige
Pfeil 3.1.5 Wiederverwendbarkeit von Views
Pfeil 3.1.6 Zeichenfarbe festlegen
Pfeil 3.2 Views und Viewcontroller
Pfeil 3.2.1 Outlets
Pfeil 3.2.2 Outlet-Collections
Pfeil 3.2.3 Containerviews
Pfeil 3.2.4 View-Hierarchien
Pfeil 3.2.5 Actions
Pfeil 3.2.6 Ereignisse
Pfeil 3.2.7 Controlzustände und Buttons
Pfeil 3.2.8 Touch-A, Touch-A, Touch Me
Pfeil 3.2.9 Übergänge
Pfeil 3.2.10 Ein kleines Intermezzo über Labels
Pfeil 3.2.11 Beliebige Objekte im Storyboard
Pfeil 3.2.12 Der Lebenszyklus eines Viewcontrollers
Pfeil 3.2.13 Speicher- und Ressourcenverwaltung des Viewcontrollers
Pfeil 3.3 Lokale Benachrichtigungen
Pfeil 3.3.1 Benachrichtigungen versenden
Pfeil 3.3.2 Benachrichtigungen verarbeiten
Pfeil 3.4 Eine App für alle
Pfeil 3.4.1 Das Retina-Display
Pfeil 3.4.2 Launch-Images
Pfeil 3.4.3 Sprachkursus für die App
Pfeil 3.4.4 Es funktioniert nicht
Pfeil 3.4.5 Unterstützung älterer iOS-Versionen
Pfeil 3.4.6 Base-Internationalisierung ausschalten
Pfeil 3.4.7 Universelle Apps
Pfeil 3.5 Geräteausrichtungen, Autosizing und Autolayout
Pfeil 3.5.1 Flexible Views dank Autosizing
Pfeil 3.5.2 Autolayout
Pfeil 3.5.3 Restriktionen im Interface-Builder festlegen
Pfeil 3.5.4 Autolayout und Lokalisierung
Pfeil 3.6 Fehlersuche
Pfeil 3.6.1 Logging
Pfeil 3.6.2 Der Debugger
Pfeil 3.6.3 Breakpoints verwalten
Pfeil 3.6.4 Die Debugger-Konsole

3Sehen und anfassenZur nächsten Überschrift

»Never put off till tomorrow what you can do the day after tomorrow.«
– Mark Twain

In diesem Kapitel werden Sie einen analogen Wecker programmieren und dabei den grundlegenden Aufbau von iOS-Apps kennenlernen. Dabei stehen die Arbeit mit Xcode und die praktische Anwendung der Grundlagen im Vordergrund, die wir in den ersten beiden Kapiteln beschrieben haben. Außerdem gestalten Sie eine einfache eigene Oberfläche, die sowohl eigene Grafiken auf den Bildschirm zeichnet als auch Nutzereingaben verarbeitet. Denn das wichtigste Merkmal eines analogen Weckers ist schließlich das Ziffernblatt mit den Zeigern.

Projektinformation

Den Quellcode des Beispielprojekts finden Sie auf der DVD unter Code/Apps/iOS7/AlarmClock oder im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Auflage_3/Apps/iOS7/AlarmClock.

Außerdem finden Sie eine Variante des Projekts unter Code/Apps/iOS7/Clocks beziehungsweise https://github.com/Cocoaneheads/iPhone/tree/Auflage_3/Apps/iOS7/Clocks.


Rheinwerk Computing - Zum Seitenanfang

3.1Eigene Viewklassen in Cocoa TouchZur nächsten ÜberschriftZur vorigen Überschrift

Legen Sie ein neues iPhone-Projekt in Xcode aus der Vorlage Single View Application an. Als Produktnamen und Präfix für die Klassennamen geben Sie »AlarmClock« ein (siehe Abbildung 3.1). Danach speichern Sie das Projekt in einem Verzeichnis Ihrer Wahl ab.

Abbildung

Abbildung 3.1 Die Parameter für das neue Projekt

Als Erstes soll der Wecker ein Ziffernblatt erhalten. Da es dafür keinen fertigen View in Cocoa Touch gibt, müssen Sie eine eigene Klasse dafür erstellen. Zum Anlegen der Klasse klicken Sie mit der rechten Maustaste auf die Gruppe AlarmClock und wählen den Punkt New File... aus (siehe Abbildung 3.2).

Abbildung

Abbildung 3.2 Hinzufügen einer neuen Klasse

Wählen Sie in der linken Spalte den Punkt Cocoa Touch in der Rubrik iOS aus. Auf der rechten Seite erscheint eine Liste mit Dateivorlagen, aus der Sie Objective-C class auswählen (siehe Abbildung 3.3).

Abbildung

Abbildung 3.3 Auswahl der Dateivorlage

Geben Sie im nächsten Schritt der Klasse den Namen ClockView, und wählen Sie als Superklasse UIView aus (siehe Abbildung 3.4); Xcode erstellt die Klassen dann als Subklasse von UIView.

Abbildung

Abbildung 3.4 Subklasse von »UIView«

Im letzten Schritt wählen Sie den Ordner, die Gruppe und das Target für die Header- und die Implementierungsdatei aus (siehe Abbildung 3.5). Hier sollte unter Group der Wert AlarmClock stehen, und unter Targets sollten Sie nur den Eintrag AlarmClock auswählen.

Abbildung

Abbildung 3.5 Anlegen der neuen Klasse in der Gruppe »AlarmClock«

Gruppen und Targets

Eine Gruppe ist jeweils eine Hierarchieebene in der Dateiansicht von Xcode. Obwohl Gruppen ein gelbes Ordnersymbol als Icon haben, entsprechen sie keinem Dateiordner auf der Festplatte. Sie können Gruppen verwenden, um die Dateien in Ihren Projekten übersichtlicher zu ordnen.

Mit einem Xcode-Projekt können Sie unterschiedliche Produkte (z. B. Programme) erzeugen. Wie Xcode dieses Produkt erstellt, beschreibt dabei ein Target. Achten Sie beim Anlegen neuer Klassen im letzten Schritt immer darauf, dass Sie die Klasse jeweils zu den gewünschten Targets hinzufügen. Andernfalls übersetzt Xcode sie nicht, und es gibt Fehlermeldungen.

Sie können die Target-Zuordnung der Implementierungsdatei übrigens in deren Dateiinspektor (siehe Abbildung 3.7) unter Target Membership überprüfen und nachträglich ändern.

Wenn Sie nach dem Anlegen der Dateien die Headerdatei der neuen Klasse öffnen, sehen Sie, dass Xcode eine Subklasse von UIView erstellt hat:

#import <UIKit/UIKit.h>

@interface ClockView : UIView

@end

Listing 3.1 Headerdatei der Klasse »ClockView«

Bevor Sie jedoch die Zeichenlogik für das Ziffernblatt implementieren, erstellen Sie die Oberfläche der App. Dann können Sie das Programm immer wieder starten und sich so die Auswirkungen Ihrer Änderungen ansehen.

Um die neue Viewklasse benutzen zu können, müssen Sie einen neuen View im View des Viewcontrollers anlegen. Diesen View finden Sie im Storyboard, das Xcode beim Anlegen des Projekts erzeugt hat. Wählen Sie im Projektnavigator die Datei Main.storyboard aus. Xcode öffnet dann den Interface-Builder und zeigt die enthaltenen Objekte an. Das ist zu Beginn ein Viewcontroller, der einen View enthält. Diese Hierarchie befindet sich in der Szene Alarm Clock View Controller Scene.

Um das Ziffernblatt in diesen View einzufügen, ziehen Sie ein Objekt der Klasse UIView – das ist das selektierte Objekt in der Objektbibliothek in Abbildung 3.6 – auf den bereits vorhandenen View. Lassen Sie den View genau dann los, wenn er exakt über dem vorhandenen View liegt. Sie erkennen diese Ausrichtung daran, dass der Interface-Builder ein blaugestricheltes Kreuz in der Mitte des Views anzeigt.

Abbildung

Abbildung 3.6 Einen neuen View im Interface-Builder anlegen

Der neue View hat automatisch dieselbe Größe wie der bereits vorhandene erhalten. Da das Ziffernblatt des Weckers aber nicht den ganzen Bildschirm ausfüllen soll, markieren Sie den gerade hinzugefügten View, indem Sie ihn anklicken, und öffnen rechts oben im Xcode-Fenster den Größeninspektor, der sich im Utilitybereich über der Objektbibliothek befindet.

(K)ein Schuss im Dunkeln

Der Größeninspektor gehört zu einer Reihe von Inspektoren, die Sie jeweils über die Leiste in Abbildung 3.7 öffnen. Alternativ können Sie auch die Tastenkombination alt+cmd+ Ziffer verwenden, wobei Sie für Ziffer jeweils die Ziffer aus der Abbildung verwenden. Sie können die Inspektoren beziehungsweise ihren Inhalt nur sehen, wenn Sie ein Element – also eine Datei, einen View etc. – ausgewählt haben.

Abbildung

Abbildung 3.7 Die Auswahlleiste für die Inspektoren

Öffnen Sie den Größeninspektor über den Button in der Leiste oder alternativ über die Tastenkombination alt+cmd+5. Danach setzen Sie die Koordinaten und die Größe des Views so wie in Abbildung 3.8 dargestellt. Er sollte eine quadratische Grundfläche von 320 × 320 Punkten haben und am Koordinatenursprung (0, 0) liegen. Der Ursprung eines Views ist immer die linke obere Ecke. Die horizontale Ausdehnung wächst nach rechts und die vertikale nach unten. Das Ziffernblatt belegt mit dieser Einstellung nur etwas mehr als die Hälfte des iPhone-Bildschirms.

Als Nächstes verknüpfen Sie den neuen View mit der Klasse, die Sie oben neu angelegt haben. Der View soll ja später das Zifferblatt anzeigen, das die Methoden der Klasse ClockView zeichnen. Lassen Sie den quadratischen View selektiert, und wählen Sie den Identitätsinspektor aus (dritter Button von links oder alt+cmd+3). Legen Sie dort über das oberste Eingabefeld, Class, die Klasse des Views fest. Sie können den Klassennamen dort entweder direkt eingeben oder aus einer Liste auswählen (siehe Abbildung 3.9). Weisen Sie dem View die neu angelegte Klasse ClockView zu.

Abbildung

Abbildung 3.8 Der Größeninspektor des Views in Xcode 5

Abbildung

Abbildung 3.9 Weisen Sie dem View die Klasse »ClockView« zu.

Der Interface-Builder kennt die Klassenhierarchie

Beim Erzeugen des Views haben Sie ein Objekt mit der Klasse UIView verwendet. Damit haben Sie bereits festgelegt, dass als Klasse für diesen View nur UIView oder eine ihrer Unterklassen in Frage kommt. Daher erscheinen auch nur genau diese Klassennamen in der Liste. Pfiffigerweise haben Sie ja die designierte Klasse ClockView als Unterklasse von UIView deklariert, und so kommt sie in der Liste vor, und alles passt.

Damit sich das Zifferblatt besser vom Hintergrund abhebt, weisen Sie dem Hauptview eine andere Hintergrundfarbe zu. Dazu selektieren Sie diesen View durch Anklicken und öffnen den Attributinspektor über den entsprechenden Button oder alt+cmd+4. Im Inspektor können Sie unter Background die Hintergrundfarbe des Views auswählen. Wenn Sie beispielsweise Hellgrau (Light Gray Color) als Farbe auswählen und das Projekt ausführen, zeigt Ihnen der Simulator eine weiße und darunter eine etwa halb so große graue Fläche an (siehe Abbildung 3.10).

Abbildung

Abbildung 3.10 Auswahl der Hintergrundfarbe über den Attributinspektor

Die Implementierungsdatei der Klasse ClockView enthält bereits zwei Methoden. Die erste Methode heißt initWithFrame: und hat einen Parameter vom Typ CGRect. Das ist ein zusammengesetzter Datentyp [Anm.: Ein C-Struct] und keine Objective-C-Klasse, der ein Rechteck über eine Koordinate und Seitenlängen (die im Folgenden auch »Größe« genannt werden) darstellt. [Anm.: In der Deklaration fehlt der Stern zwischen dem Klassennamen und der Variablen.]

Dieser Typ hat Klasse

Sie können einfache Datentypen und C-Strukturen in Cocoa nicht am Namen von Klassen unterscheiden. Zum Beispiel gibt es eine Klasse NSNumber und einen einfachen Datentyp NSInteger. Es gibt jedoch keinen Hinweis im Namen, ob es sich um eine Klasse oder Struktur handelt.

Objective-C erlaubt nur Zeiger als Verweis auf Objekte, weswegen die Deklaration NSNumber theNumber; zu der Fehlermeldung

interface type cannot be statically allocated

führt. Umgekehrt dürfen Sie dagegen Zeiger auf einfache Datentypen deklarieren.

Die Deklaration NSInteger *theInteger; ist erlaubt, und erst bei der Zuweisung erhalten Sie in der Regel eine Warnung. Falls Sie sich nicht sicher sind, was ein Name im Quelltext beschreibt, gelangen Sie durch cmd + linke Maustaste zur Deklaration dieses Namens. Das funktioniert praktischerweise mit fast allen Bezeichnern im Quelltext. Sie können auch alt + linke Maustaste drücken, um eine Dokumentation zu dem Bezeichner zu erhalten.

In Objective-C gibt es keine Konstruktoren, die Sie vielleicht aus anderen Programmiersprachen kennen. Die Objekterzeugung ist vielmehr in Speicherplatzreservierung und Objektinitialisierung aufgeteilt. Die Klassenmethode alloc zur Speicherplatzreservierung haben Sie bereits kennengelernt. Sie reserviert den notwendigen Speicherplatz und füllt ihn mit Nullen.

Initialisierungsmethoden kennen Sie ebenfalls schon seit dem zweiten Kapitel. In Objective-C heißen sie per Konvention entweder init oder haben das Präfix init. Wenn Sie einen View über den Programmcode anlegen, sollten Sie in der Regel die Initialisierungsmethode initWithFrame: verwenden, sofern die Dokumentation nicht andere Initialisierungsmethoden empfiehlt. Der Parameter ist ein Rechteck, das die Größe und Position des neuen Views relativ zu dessen umgebendem View, dem Superview, angibt. Da Cocoa Touch die Views in einer Hierarchie anordnet, hat fast jeder View einen umgebenden View; das ist jeweils dessen Vorgänger in der Hierarchie. Beispielsweise hat im Beispielprojekt der Clockview den View des Viewcontrollers als Superview. Der Aufbau der Methode initWithFrame: ist im Allgemeinen wie folgt:

- (id)initWithFrame:(CGRect)inFrame {
self = [super initWithFrame:inFrame];
if(self != nil) {
// Initialisierung des Objekts
}
return self;
}

Listing 3.2 Initialisierungsmethode eines Views

Viewgeometrie

Die Klasse UIView besitzt zwei Rechtecke, die die Position und die Ausmaße des Views beschreiben. Die Property frame enthält, wie bereits beschrieben, die Position und die Größe des Views relativ zu seinem umgebenden View. Die Position des Frames gibt also die Entfernung der oberen linken Ecke des Views von der oberen linken Ecke seines umgebenden Views an (siehe Abbildung 3.11, links).

Der gezeichnete View muss hingegen nicht auf dem Rechteck liegen, das der Frame beschreibt. Diese Koordinaten gibt das Rechteck in der Property bounds an.

Die Bounds beschreiben also die Lage des Views relativ zu seinem Frame. In den meisten Fällen ist die Position der Bounds der Nullpunkt (0, 0), und die Größe entspricht der Größe des Frames (Abbildung 3.11, rechts). Das muss jedoch nicht immer so sein; beispielsweise bei skalierten Views trifft das häufig nicht zu.

Abbildung

Abbildung 3.11 Rechteck für Frame und Bounds

Das klingt recht kompliziert. Für den Anfang können Sie sich zwei einfache Faustregeln merken:

  1. Sie verändern die Größe und die Position eines Views in der Regel über seinen Frame.
  2. Wenn Sie die Koordinaten eines Views relativ zu seinem umgebendem View berechnen möchten, verwenden Sie die Bounds des umgebenden Views, um den Frame des enthaltenen Views zu berechnen.

Das werden wir später noch an ein paar Beispielen erläutern.

Die Methode in Listing 3.2 ruft als Erstes die Initialisierungsmethode der Oberklasse auf und weist deren Ergebnis der Variablen self zu. Danach überprüft sie, ob self nicht nil ist. Wenn self auf ein Objekt zeigt, kann sie das Objekt initialisieren und schließlich auch zurückgeben.

Vielleicht überrascht es Sie, dass die Initialisierungsmethode self verändern kann oder nil zurückliefern darf. Das ist indes keine theoretische Möglichkeit, sondern wird auch eingesetzt. Eine Initialisierungsmethode sollte beispielsweise nil zurückgeben, wenn sie das Objekt nicht initialisieren kann. Die Initialisierungsmethode initWithString: der Klasse NSURL gibt beispielsweise nil zurück, wenn die angegebene URL einen syntaktischen Fehler enthält.

Die Klasse ClockView enthält in einem Kommentarblock eine weitere Methode namens drawRect:. Entfernen Sie die Kommentarzeichen /* und */ um diese Methode sowie alle Kommentarzeilen, also Zeilen, die mit // beginnen.

- (void)drawRect:(CGRect)rect
{
}

@end

Listing 3.3 Die Methode »drawRect:« ohne Kommentare

Das Rechteck, das Cocoa Touch als Parameter an die Methode übergibt, beschreibt den Teil des Views, den die Methode neu zeichnen soll. Es muss also nicht den Bounds des Views entsprechen, sondern kann auch nur ein Teil davon sein. In den meisten Fällen können Sie diesen Parameter einfach ignorieren, da dieses Rechteck nur für eine Reduktion der Zeichenoperationen bei optimierter Ausgabe interessant ist. Das ist in der Regel jedoch nur bei sehr großen oder aufwendig zu zeichnenden Views notwendig.


Rheinwerk Computing - Zum Seitenanfang

3.1.1Zeichnen in Cocoa TouchZur nächsten ÜberschriftZur vorigen Überschrift

Als Nächstes erstellen Sie den Code, um alle Bestandteile der Uhr – also Ziffernblatt und die Uhrzeiger – zu zeichnen. Dafür verwenden Sie das Core-Graphics-Framework. Core Graphics ist ein sehr mächtiges Framework, das auf reinem C und nicht auf Objective-C basiert. Deshalb enthält es keine Klassen und Methoden, sondern Datentypen, Strukturen und Funktionen. Alle zum Core-Graphics-Framework gehörenden Elemente erkennen Sie am Präfix CG.

Der Aufbau und die Implementierung der Datentypen in den Frameworks sind vor dem Nutzer versteckt. Sie können auf die Datentypen nur über Referenzen zugreifen. Die Referenzen übergeben Sie an Funktionen, die eine bestimmte Operation ausführen. Auf diese Art implementierte Datentypen nennt man auch opake Datentypen.

Der zentrale Datentyp in Core Graphics ist der Grafikkontext, und die Referenzen darauf haben den Typ CGContextRef. Ein Grafikkontext ist eine Zeichenfläche und speichert auch alle Werte für Ihre Zeichenoperationen, die Sie darauf ausführen. Wenn Sie eine Zeichenoperation über einen Grafikkontext ausführen möchten, müssen Sie also zuerst die Parameter wie Farben, Muster und Clipping festlegen und danach die gewünschte Operation aufrufen. Für das Ziffernblatt werden Sie zunächst einen weißen Kreis als Grundfläche zeichnen. Die entsprechenden Core-Graphics-Operationen dazu sehen wie folgt aus:

- (void)drawRect:(CGRect)inRectangle {
CGContextRef theContext = UIGraphicsGetCurrentContext();
CGRect theBounds = self.bounds;
CGContextSaveGState(theContext);
CGContextSetRGBFillColor(theContext, 1.0, 1.0, 1.0, 1.0);
CGContextAddEllipseInRect(theContext, theBounds);
CGContextFillPath(theContext);
CGContextRestoreGState(theContext);
}

Listing 3.4 Zeichnen einer weißen Ellipse

In der ersten Anweisung weist die Methode der Variablen theContext über die Funktion UIGraphicsGetCurrentContext den aktuellen Grafikkontext zu, in den die Ausgabe erfolgt. Cocoa Touch erzeugt den Kontext vor dem Aufruf der drawRect:-Methode, und Sie dürfen drawRect: daher niemals direkt aufrufen, da dann kein Kontext zur Verfügung steht. Wenn Sie einen View neu zeichnen lassen möchten, sollten Sie stattdessen die Methode setNeedsDisplay des Views aufrufen. Cocoa Touch zeichnet den View dann neu, sobald das möglich ist.

Views neu zeichnen

Dieses Verfahren, Views über setNeedsDisplay zum Neuzeichnen zu markieren, klingt zunächst einmal umständlich, hat jedoch einige Vorteile. Zum einen zeichnet Cocoa Touch den View erst dann neu, wenn es ihm in den Kram passt, das Neuzeichnen blockiert also keine wichtigeren Aufgaben des Betriebssystems oder des Programms. Außerdem können Sie beliebig viele Änderungen an Ihrem View vornehmen, ohne ihn jedesmal neu zeichnen zu lassen. Bei jeder Änderung markieren Sie jeweils nur den View, und Cocoa Touch zeichnet ihn danach nur einmal neu. Sie brauchen sich also nicht darum zu kümmern, wann das Programm den View neu zeichnet, sondern nur darum, ob.

Da die Größe des Ziffernblattes von der Größe des Views abhängt, ermittelt die Methode die Koordinaten und die Größe des Views in der Variablen theBounds. Die Koordinaten des Ziffernblattes liegen relativ zu den Koordinaten seines Views, deshalb müssen Sie hier die Bounds und nicht den Frame für die Berechnung verwenden.

In der nächsten Zeile sichern Sie den aktuellen Zustand des Grafikkontexts über die Funktion CGContextSaveGState. Damit lässt sich am Ende der Methode genau dieser Zustand des Kontexts sehr einfach über die Funktion CGContextRestoreGState wiederherstellen. Da Sie nicht wissen, welche Zeichenoperationen in dem Grafikkontext Cocoa Touch nach der Ausführung Ihrer drawRect:-Methode noch ausführt, sollten Sie immer darauf achten, den Kontext so zu hinterlassen, wie Sie ihn vorgefunden haben. (Ganz anders als die Toiletten der Deutschen Bahn, die man ja so hinterlassen soll, wie man sie vorzufinden wünscht. Dummerweise hat man nie einen Dampfstrahler dabei, wenn man ihn braucht.) Achten Sie bei der Verwendung dieser beiden Funktionen aber bitte immer darauf, ihre Aufrufe zu balancieren; das heißt, auf jeden CGContextSaveGState-Aufruf sollte immer genau ein CGContextRestoreGState-Aufruf folgen.

Die folgenden Zeilen zeichnen dann den Kreis des Ziffernblattes, indem die Methode zuerst die Füllfarbe als RGBA-Werte [Anm.: RGBA steht für Red, Green, Blue und Alpha; die einzelnen Farbkomponenten.] über die Funktion CGContextSetRGBAFillColor setzt. Die Fließkommawerte für die Farbkomponenten (Rot, Grün und Blau) und den Alphawert, der die Deckkraft festlegt, dürfen dabei zwischen 0 und 1 liegen. Die Funktion CGContextAddEllipseInRect fügt dem Kontext einen Pfad in Form einer Ellipse hinzu. Bei einem quadratischen Ziffernblatt haben die beiden Radien dieser Ellipse die gleiche Länge, und es entsteht ein Spezialfall der Ellipse – ein Kreis. Durch den Aufruf der Funktion CGContextFillPath zeichnet schließlich Core Graphics einen ausgefüllten Kreis mit der gesetzten Füllfarbe.

Pfade in Core Graphics

Sie zeichnen Linien und Flächen in Core Graphics, indem Sie im Grafikkontext zuerst den Linienzug der Figur beschreiben. Danach rufen Sie eine Zeichenoperation für den Pfad auf, mit der Sie den Linienzug über CGContextStrokePath zeichnen oder ihn über CGContextFillPath ausfüllen. Eine weitere mögliche Operation ist das Clippen über die Funktion CGContextClip. Dabei beschneiden Sie die Zeichenfläche, so dass der Kontext nur noch im Bereich des Linienzuges zeichnet.

Nach der Ausführung der Zeichenoperation vergisst der Kontext übrigens den Pfad. Wenn Sie also einen ausgefüllten Kreis mit einem Rand zeichnen möchten, müssen Sie den Pfad des Kreises jeweils vor den beiden Zeichenoperationen zu dem Kontext hinzufügen.

Das Programm zeigt bei der Ausführung jetzt allerdings immer noch ein weißes Quadrat und ein graues Rechteck an. Das liegt daran, dass der View für das Ziffernblatt ebenfalls Weiß als Hintergrundfarbe verwendet. Er stellt also einen weißen Kreis vor einem weißen Hintergrund dar.

Damit Sie den Kreis sehen können, ändern Sie die Hintergrundfarbe des Views. Öffnen Sie dazu den View im Interface-Builder, und wählen Sie den View für das Ziffernblatt aus, indem Sie auf die entsprechende Stelle des Hauptviews klicken. Mit dem Attributinspektor, der zwischen dem Identitäts- und dem Größeninspektor liegt, verändern Sie über das Attribut Background die Hintergrundfarbe des Views (siehe Abbildung 3.10). Wählen Sie ein freundliches Grau oder die Transparenz (Clear Color) aus. Bei einem transparenten Hintergrund erscheint an den Stellen, die der Kreis nicht verdeckt, die Hintergrundfarbe des umgebenden Views.

Die Änderung lässt sich umgehend im Simulator überprüfen: Das Ziffernblatt ist jetzt sichtbar (siehe Abbildung 3.12) – ein schöner runder Kreis.

Vollbild in iOS 7

In iOS 7 hat Apple für Viewcontroller die Vollbildansicht eingeführt. Das bedeutet, dass sich ein Viewcontroller bis zum oberen Rand des Displays erstreckt, und damit von der iOS-Titelleiste überdeckt wird (wie in Abbildung 3.12 zu sehen).

Das Ziffernblatt ist in diesem Stadium noch sehr minimalistisch. Im nächsten Schritt fügen Sie die Zeichenoperationen für die Darstellung der Stunden- und Minuteneinteilung hinzu. Dazu braucht die Klasse ClockView zwei neue Methoden, die Sie vor der drawRect:-Methode einfügen:

- (CGPoint)midPoint {
CGRect theBounds = self.bounds;
return CGPointMake(CGRectGetMidX(theBounds),
CGRectGetMidY(theBounds));
}

Listing 3.5 Berechnung des Mittelpunktes des Views

Die Methode midPoint berechnet den Mittelpunkt des Views relativ zum Bounds-Rechteck. Die Klasse UIView hat zwar eine ähnliche Methode center, die allerdings den Mittelpunkt des frame-Rechtecks liefert. Sie ist hierfür jedoch nicht geeignet, da Sie ja das Ziffernblatt relativ zum View zeichnen müssen. Für solche Berechnungen stellt Core Graphics einige praktische Funktionen, wie CGRectGetMidX und CGRectGetMidY, zur Verfügung. Den Punkt vom Typ CGPoint erstellen Sie schließlich über die Funktion CGPointMake.

Abbildung

Abbildung 3.12 Das Ziffernblatt ist fast fertig.

Die zweite Methode, pointWithRadius:angle:, berechnet einen Punkt, der vom Mittelpunkt in einem angegebenen Winkel und einer angegebenen Entfernung liegt. Dabei erfolgt die Angabe des Winkels im Bogenmaß, im Uhrzeigersinn und ausgehend von 12 Uhr gerechnet. Einen Winkel w in Grad können Sie über die Formel w π / 180° ins Bogenmaß umrechnen.

- (CGPoint)pointWithRadius:(CGFloat)inRadius
angle:(CGFloat)inAngle {
CGPoint theCenter = [self midPoint];
return CGPointMake(theCenter.x + inRadius * sin(inAngle),
theCenter.y – inRadius * cos(inAngle));
}

Listing 3.6 Berechnung eines Punktes über einen Radius und einen Winkel

Das Ziffernblatt stellt einen halben Tag, also 12 Stunden, dar. Somit entsprechen 12 Stunden einem vollen Kreis von 360°, und eine Stunde entspricht

360° / 12 = 30°

oder

30° π / 180° = π / 6

im Bogenmaß. Eine Stunde hat 60 Minuten, und damit hat eine Minute einen Winkel von

360° / 60 = 6°

oder

6° π / 180° = π / 30

im Bogenmaß. Für Sekunden gelten natürlich die gleichen Winkelwerte wie für Minuten.

Mathematik für Einsteiger

Sie fragen sich möglicherweise, was die viele Mathematik im ersten größeren Beispiel soll. Mathematik begegnet einem Programmierer häufiger, als ihm bisweilen lieb ist. Gerade bei der Erstellung von Frontends und der Arbeit mit Grafiken und Animationen ist in der Regel ein gutes mathematisches Grundverständnis gefragt. Die im Wecker-Beispiel angewandte Mathematik ist ja kein Selbstzweck, sondern resultiert direkt aus der Verwendung des Core-Graphics-Frameworks. Eine kleine Auffrischung Ihres Wissens über Bogenmaß, Winkelberechnung etc. finden Sie bei Wikipedia3.

Beispielsweise hat ein Zeiger auf »drei Uhr« den Winkel 90° beziehungsweise π / 2 im Bogenmaß oder auf »10 Minuten« den Winkel 60° beziehungsweise π / 3. Die Position eines Uhrzeigers oder eines Strichs auf dem Ziffernblatt kann also durch die Angabe eines Winkels und ein oder mehrere Längenangaben bestimmt werden. Diese Beispiele stellt Abbildung 3.13 grafisch dar.

Abbildung

Abbildung 3.13 Beziehung zwischen Uhrzeiten und Winkeln

Das Ziffernblatt soll Minuten durch einen Punkt und Stunden durch einen Strich darstellen. Die Striche für drei, sechs, neun und zwölf Uhr sollen dabei etwas länger als die sonstigen Stundenstriche sein. Die Einteilungen werden über eine Schleife gezeichnet, die von 0 bis 59 läuft; für jede Minute ist jeweils ein Durchlauf vorgesehen.

Da alle Einteilungen die gleiche Farbe, Linienstärke und Linienenden haben sollen, können Sie diese Einstellungen schon vor der Schleife festlegen. Beachten Sie, dass die Stundeneinteilungen Linien und die Minuteneinteilungen Kreise sind. Die Linienfarbe müssen Sie über CGContextSetRGBStrokeColor setzen. Die Stundenstriche sollen außerdem abgerundete Linienenden haben, was Sie über die Funktion CGContextSetLineCap festlegen.

Da diese runden Kappen jedoch auf beiden Enden der Linien sitzen, reichen sie über den Rand des Ziffernblattes hinaus. Sie können das durch ein Clipping verhindern. Das ist eine unsichtbare Maske auf der Zeichenfläche, mit der Sie die Zeichenfläche in Bereiche unterteilen. Der Grafikkontext zeichnet dann nur in den Bereichen, die zum Clipping gehören. Als Clipping-Region verwenden Sie den Kreis des Ziffernblattes. Sie fügen dessen Pfad erneut dem Kontext hinzu und setzen ihn durch Aufruf von CGContextClip als Clipping-Pfad. Listing 3.7 enthält die soeben beschriebenen Funktionsaufrufe für die Kontexteinstellungen:

CGContextSetRGBStrokeColor(theContext, 0.25, 0.25, 0.25, 1.0);
CGContextSetRGBFillColor(theContext, 0.25, 0.25, 0.25, 1.0);
CGContextSetLineWidth(theContext, 8.0);
CGContextSetLineCap(theContext, kCGLineCapRound);
CGContextAddEllipseInRect(theContext, theBounds);
CGContextClip(theContext);

Listing 3.7 Setzen von Zeichenattributen im Grafikkontext

Die Schleife zum Zeichnen der Einteilungen sieht nun folgendermaßen aus:

CGFloat theRadius = CGRectGetWidth(theBounds) / 2.0;
for(NSInteger i = 0; i < 60; ++i) {
CGFloat theAngle = i * M_PI / 30.0; // eine Minute
if(i % 5 == 0) { // alle 5 Minuten ein Strich
CGFloat theInnerRadius = theRadius *
(i % 15 == 0 ? 0.7 : 0.8);
CGPoint theInnerPoint =
[self pointWithRadius:theInnerRadius
angle:theAngle];
CGPoint theOuterPoint = [self
pointWithRadius:theRadius
angle:theAngle];
CGContextMoveToPoint(theContext, theInnerPoint.x,
theInnerPoint.y);
CGContextAddLineToPoint(theContext, theOuterPoint.x,
theOuterPoint.y);
CGContextStrokePath(theContext);
}
else {
CGPoint thePoint =
[self pointWithRadius:theRadius * 0.95
angle:theAngle];
CGContextAddArc(theContext, thePoint.x, thePoint.y,
4.0, 0.0, 2 * M_PI, YES);
CGContextFillPath(theContext);
}
}

Listing 3.8 Zeichnen eines Ziffernblattes

Anhand der Schleifenvariablen i können Sie mit Hilfe des Divisionsrestoperators % entscheiden, um welchen Einteilungstyp es sich handelt. Wenn ihr Wert durch 5 teilbar ist (i % 5 == 0), muss es ein Strich sein. Ist sie außerdem auch durch 15 teilbar (i % 15 == 0), so muss es ein langer Strich sein. Alle anderen Werte sind Punkte. Die Länge der Striche und die Lage der Punkte errechnen sich dabei relativ zum Radius des Ziffernblattes.

Teilbarkeit

Ob eine ganze Zahl ein Teiler einer anderen Zahl ist, können Sie über den Divisionsrestoperator % ermitteln. Wenn der Rest einer Division 0 ist, dann ist der Dividend durch den Divisor teilbar. Beispielsweise prüft der Ausdruck i % 5 == 0, ob der Dividend i durch den Divisor 5 teilbar ist.

Um die Linien jeweils am gewünschten Punkt beginnen zu lassen, müssen Sie den Startpunkt der Linie setzen, ohne den Pfad des Kontexts zu erweitern. Dazu verwenden Sie die Funktion CGContextMoveToPoint.

Zum Zeichnen der Punkte verwendet der Code die Funktion CGContextAddArc. Es ist hier einfacher, den Kreis über seinen Mittelpunkt und Radius zu bestimmen als über sein umgebendes Rechteck. Über diese Funktion können Sie beliebige Kreisbögen erzeugen, weswegen Sie den Start- und den Endwinkel des Bogens im Bogenmaß angeben müssen. Damit Sie prüfen können, ob Sie auf dem richtigen Weg sind, sehen Sie hier noch einmal die ganze Methode drawRect: an einem Stück, und die Ausgabe sehen Sie in Abbildung 3.14.

- (void)drawRect:(CGRect)inRectangle {
CGContextRef theContext = UIGraphicsGetCurrentContext();
CGRect theBounds = self.bounds;
CGFloat theRadius = CGRectGetWidth(theBounds) / 2.0;

CGContextSaveGState(theContext);
CGContextSetRGBFillColor(theContext, 1.0, 1.0, 1.0, 1.0);
CGContextAddEllipseInRect(theContext, theBounds);
CGContextFillPath(theContext);
CGContextAddEllipseInRect(theContext, theBounds);
CGContextClip(theContext);
CGContextSetRGBStrokeColor(theContext,
0.25, 0.25, 0.25, 1.0);
CGContextSetRGBFillColor(theContext,
0.25, 0.25, 0.25, 1.0);
CGContextSetLineWidth(theContext, 8.0);
CGContextSetLineCap(theContext, kCGLineCapRound);
for(NSInteger i = 0; i < 60; ++i) {
CGFloat theAngle = i * M_PI / 30.0;

if(i % 5 == 0) {
CGFloat theInnerRadius = theRadius *
(i % 15 == 0 ? 0.7 : 0.8);
CGPoint theInnerPoint =
[self pointWithRadius:theInnerRadius
angle:theAngle];
CGPoint theOuterPoint =
[self pointWithRadius:theRadius
angle:theAngle];
CGContextMoveToPoint(theContext, theInnerPoint.x,
theInnerPoint.y);
CGContextAddLineToPoint(theContext,
theOuterPoint.x,
theOuterPoint.y);
CGContextStrokePath(theContext);
}
else {
CGPoint thePoint =
[self pointWithRadius:theRadius * 0.95
angle:theAngle];
CGContextAddArc(theContext,thePoint.x, thePoint.y,
3.0, 0.0, 2 * M_PI, YES);
CGContextFillPath(theContext);
}
}
[self drawClockHands];
CGContextRestoreGState(theContext);
}

Listing 3.9 Die komplette Methode zum Zeichnen des Ziffernblattes

Abbildung

Abbildung 3.14 Das Ziffernblatt des Weckers

Die Methode drawClockHands, deren Aufruf am Ende von drawRect: erfolgt, zeichnet die Zeiger des Weckers. Für die Implementierung müssen Sie die aktuelle Systemzeit in Winkel umrechnen, damit Sie die Zeiger durch Linien darstellen können.


Rheinwerk Computing - Zum Seitenanfang

3.1.2ZeitberechnungZur nächsten ÜberschriftZur vorigen Überschrift

Der ClockView soll die angezeigte Zeit in der Property time speichern. Diese hat den Typ NSDate, dessen Objekte einen Zeitpunkt enthalten, der auf einem Referenzzeitpunkt basiert. Da die Property das Datumsobjekt halten muss, bekommt sie den Speicherverwaltungstyp strong. Das Datumsobjekt enthält den Zeitpunkt als Anzahl der Sekunden zu einem festgelegten Referenzzeitpunkt – dem 1. Januar 2001 in der Zeitzone Greenwich Mean Time (GMT). Hingegen ist die Umrechnung des Zeitpunktes in eine Datumsdarstellung sehr schwer. Dafür gibt es jedoch glücklicherweise spezielle Klassen. Diese Darstellung von Datumswerten klingt zunächst unnötig kompliziert, hat indes einige Vorteile:

  • Sie können mit Datumsobjekten gut rechnen. Beispielsweise können Sie einen weiteren Zeitpunkt durch Addition eines positiven oder negativen Zeitintervalls berechnen.
  • Der Wert eines Zeitpunktes hat überall auf der Welt und zu jeder Zeit die gleiche Bedeutung.
  • Es gibt sehr viele unterschiedliche Zeitzonen und Kalender, wie beispielsweise den buddhistischen, gregorianischen und japanischen Kalender. Es gibt also sehr viele verschiedene Darstellungen des gleichen Zeitpunktes. Es ist einfacher, einen Zeitpunkt in eine Datumsdarstellung umzuwandeln als eine Datumsdarstellung in eine andere.
  • Die internen Uhren aller Computer – auch die der iPhones und iPads – und der Atomuhren stellen die aktuelle Zeit als Differenz zu einem Referenzzeitpunkt dar. Für die Erzeugung eines Datumsobjekts braucht der Computer also nicht viel Rechenzeit.

Um einen Zeitpunkt in eine Datumsdarstellung umzurechnen, gibt es zwei Klassen im Foundation-Framework:

  • NSDateFormatter
  • NSCalendar

Mit NSDateFormatter können Sie zu einem NSDate-Objekt eine Zeichenkette erzeugen, die das enthaltene Datum darstellt. Diese Klasse haben Sie bereits im zweiten Kapitel verwendet, und Sie kommt auch im Beispielprojekt Fototagebuch zum Einsatz.

Mit Objekten der Klasse NSCalendar berechnen Sie basierend auf einer Zeitzone und einer Region die Datumskomponenten zu einem Zeitpunkt und umgekehrt. Mit dieser Klasse und ein bisschen Rechnerei können Sie also die Positionen der Uhrzeiger zu einem Zeitpunkt bestimmen. Aus diesem Grund enthält die Klasse eine weitere, haltende Property namens calendar vom Typ NSCalendar. Die Deklaration für die beiden neuen Propertys time und calendar fügen Sie, wie in Listing 3.10 gezeigt, in die Klassendeklaration in der Datei ClockView.h ein.

@interface ClockView : UIView

@property (nonatomic, strong) NSDate *time;
@property (nonatomic, strong) NSCalendar *calendar;

@end

Listing 3.10 Property für den Kalender anlegen

Mit der Methode components:fromDate: aus der Kalenderklasse lassen Sie ein NSDateComponents-Objekt berechnen, das die benötigten Zeitkomponenten (Stunde, Minute und Sekunde) enthält. Mit den oben angegebenen Formeln bestimmen Sie daraus dann die entsprechenden Winkel für die Darstellung.

Wenn Sie allerdings die Stunde in einen Winkel umrechnen, bekommen Sie einen Stundenzeiger, der immer nur auf volle Stunden zeigt. Bei einer analogen Uhr soll er jedoch beispielsweise um halb fünf genau zwischen der vierten und fünften Stunde stehen. Das können Sie erreichen, indem Sie zum Stundenwert den Minutenanteil hinzurechnen. Da eine Stunde bekanntlich 60 Minuten hat, ist der Minutenanteil einer Stunde ein Sechzigstel des Minutenwertes.

NSDateComponents *theComponents =
[self.calendar components:NSHourCalendarUnit |
NSMinuteCalendarUnit |
NSSecondCalendarUnit
fromDate:self.time];
CGFloat theSecond = theComponents.second * M_PI / 30.0;
CGFloat theMinute = theComponents.minute * M_PI / 30.0;
CGFloat theHour = (theComponents.hour +
theComponents.minute / 60.0) * M_PI / 6.0;

Listing 3.11 Berechnung der Winkel für Sekunde, Minute und Stunde

Die Variablen theSecond, theMinute und theHour enthalten den Winkel für die entsprechende Zeigerstellung. Der Faktor π / 30 = 2 π / 60 in der Berechnung entspricht dabei dem Winkel im Bogenmaß von einer Sekunde beziehungsweise einer Minute. Analog ist π / 6 = 2 π / 12 der Winkel, der einer vollen Stunde auf einem Ziffernblatt entspricht.

Ist er Ihr Typ?

Bei der Berechnung des Minutenanteils verwendet Listing 3.11 nicht die Konstante 60, sondern 60.0 als Teiler. Der Unterschied dieser beiden Werte ist ihr Datentyp. Während 60 den Typ int hat, ist 60.0 ein double. Da der Minutenwert theComponents.minute den Typ NSInteger hat, würde bei einer Division durch 60 eine Ganzzahldivision ausgeführt. Der Minutenwert liegt immer zwischen 0 und 59, bei einer Ganzzahldivision kommt dabei also immer – Tusch – 0 heraus. Die Rechnung können Sie sich also auch sparen. Bei einer Fließkommadivision erhalten Sie hingegen Werte zwischen 0 und 0,99.

Jetzt brauchen Sie nur noch die drei Zeiger zu zeichnen, indem Sie eine Linie vom Mittelpunkt des Views in Richtung des jeweiligen Winkels ziehen. Die drei Linien unterscheiden sich jedoch nicht nur hinsichtlich ihres Winkels, sondern auch in der Länge, der Farbe und der Linienstärke.

Den Zielpunkt der Zeiger berechnen Sie jeweils über die Methode pointWithRadius:angle:, wobei Sie die unterschiedlichen Längen durch die Verwendung unterschiedlicher Radien erzielen. Das Zeichnen der einzelnen Zeiger ist recht ähnlich und unterscheidet sich nur in den unterschiedlichen Zeichenattributen für die Linien. Die gesamte Methode drawClockHands sieht dann wie folgt aus:

- (void)drawClockHands {
CGContextRef theContext = UIGraphicsGetCurrentContext();
CGPoint theCenter = [self midPoint];
CGFloat theRadius = CGRectGetWidth(self.bounds) / 2.0;
NSDateComponents *theComponents =
[self.calendar components:NSHourCalendarUnit |
NSMinuteCalendarUnit |
NSSecondCalendarUnit
fromDate:self.time];
CGFloat theSecond = theComponents.second * M_PI / 30.0;
CGFloat theMinute = theComponents.minute * M_PI / 30.0;
CGFloat theHour = (theComponents.hour +
theComponents.minute / 60.0) * M_PI / 6.0;
// Stundenzeiger zeichnen
CGPoint thePoint = [self pointWithRadius:theRadius * 0.7
angle:theHour];

CGContextSetRGBStrokeColor(theContext,
0.25, 0.25, 0.25, 1.0);
CGContextSetLineWidth(theContext, 8.0);
CGContextSetLineCap(theContext, kCGLineCapButt);
CGContextMoveToPoint(theContext, theCenter.x, theCenter.y);
CGContextAddLineToPoint(theContext, thePoint.x, thePoint.y);
CGContextStrokePath(theContext);
// Minutenzeiger zeichnen
thePoint = [self pointWithRadius:theRadius * 0.9
angle:theMinute];
CGContextSetLineWidth(theContext, 4.0);
CGContextMoveToPoint(theContext, theCenter.x, theCenter.y);
CGContextAddLineToPoint(theContext, thePoint.x, thePoint.y);
CGContextStrokePath(theContext);
// Sekundenzeiger zeichnen
thePoint = [self pointWithRadius:theRadius * 0.95
angle:theSecond];
CGContextSetLineWidth(theContext, 2.0);
CGContextSetRGBStrokeColor(theContext, 1.0, 0.0, 0.0, 1.0);
CGContextMoveToPoint(theContext, theCenter.x, theCenter.y);
CGContextAddLineToPoint(theContext, thePoint.x, thePoint.y);
CGContextStrokePath(theContext);
}

Listing 3.12 So werden die Uhrzeiger des Weckers gezeichnet.


Rheinwerk Computing - Zum Seitenanfang

3.1.3View-Erzeugung über StoryboardsZur nächsten ÜberschriftZur vorigen Überschrift

Sie müssen den beiden Propertys calendar und time vor der ersten Anzeige des Views nun noch sinnvolle Werte zuweisen, damit Sie den neuen Code auch testen können. Dazu erweitern Sie die Initialisierungsmethoden entsprechend.

Der Convenience-Konstruktor date der Klasse NSDate erzeugt ein Datumsobjekt mit der aktuellen Uhrzeit, und die Klassenmethode currentCalendar von NSCalendar liefert ein Kalenderobjekt, das den aktuellen Einstellungen des iPhones entspricht.

- (id)initWithFrame:(CGRect)inFrame {
self = [super initWithFrame:inFrame];
if(self) {
self.calendar = [NSCalendar currentCalendar];
self.time = [NSDate date];
}
return self;
}

Listing 3.13 Initialisierung der Propertys für die Zeitanzeige

Ihr Programm verwendet die Initialisierungsmethode initWithFrame: allerdings nur, wenn Sie Ihren View direkt über Programmcode erzeugen. Die App lädt diesen View jedoch aus einem Storyboard und ruft dabei diese Methode nicht auf.

Für diesen Fall gibt es zwei mögliche Alternativen. Sie können eine weitere Initialisierungsmethode namens initWithCoder: oder eine Methode namens awakeFromNib implementieren. Sie dürfen dabei eine oder auch beide Methoden verwenden. Die Initialisierung eines Objekts aus einem Storyboard erfolgt über initWithCoder:, sofern sie diese Methode besitzt, ansonsten kommt die Initialisierungsmethode init zum Einsatz. Da die Klasse UIView das Protokoll NSCoder adaptiert und somit diesen Initializer bereitstellt, erfolgt die Initialisierung immer darüber. Sind alle Objekte aus der Storyboard-Szene geladen und initialisiert, sendet Cocoa Touch an jedes Objekt die Nachricht awakeFromNib, die es von NSObject erbt. Diese Methode können Sie also überschreiben, um ein Objekt erst dann zu initialisieren, wenn Cocoa Touch alle Objekte aus dem Storyboard geladen hat. Den Ablauf für die Initialisierung eines Views stellt Abbildung 3.15 grafisch dar.

Abbildung

Abbildung 3.15 Ablauf der Initialisierung für Views aus NIB-Dateien

In einem Storyboard sind alle Objekte als serialisierte Daten abgelegt. Die Serialisierung enthält alle notwendigen Informationen, um diese Objekte wiederherzustellen. Zu diesen Informationen gehören die Klassen, die Attributwerte und die Anordnung der Objekte zueinander. Die Storyboards im Projekt verwenden dafür ein XML-Format. Wenn Xcode das Programm erstellt, erzeugt es aus der XML-Darstellung eine binäre Datei, die weniger Platz braucht und das Programm auch schneller verarbeiten kann.

Wenn die Deserialisierung die Methode initWithCoder: des neuen Objekts aufruft, übergibt sie einen Parameter vom Typ NSCoder mit den notwendigen Daten für dieses Objekt. Da Ihre Klasse jedoch die Serialisierung nicht erweitert, brauchen Sie den Parameter nur für den Initialisierer-Aufruf der Oberklasse. Wenn Sie die Methode initWithCoder: implementieren, sollten Sie immer als Erstes diese Methode der Oberklasse so wie in Listing 3.14 aufrufen:

- (id)initWithCoder:(NSCoder *)inCoder {
self = [super initWithCoder:inCoder];
if(self) {
self.calendar = [NSCalendar currentCalendar];
self.time = [NSDate date];
}
return self;
}

Listing 3.14 Initialisierung der Propertys bei der Deserialisierung

Nach der Erzeugung aller Objekte aus einem Storyboard ruft Cocoa Touch die Methode awakeFromNib bei allen erzeugten Objekten auf. Wenn Sie diese Methode überschreiben, sollten Sie auch immer als Erstes die Methode in der Oberklasse aufrufen.

- (void)awakeFromNib {
[super awakeFromNib];
self.calendar = [NSCalendar currentCalendar];
self.time = [NSDate date];
}

Listing 3.15 Initialisierung der Propertys nach der Deserialisierung

Verlassen Sie sich weder bei initWithCoder: noch bei awakeFromNib auf eine feste Aufrufreihenfolge bei unterschiedlichen Views. Für die Initialisierung ist häufig awakeFromNib die geeignetere Wahl, da bei Aufruf dieser Methode auch alle anderen Objekte des NIB-Files die Initialisierung durchlaufen haben.

Da die Klasse die Initialisierung der beiden Propertys nur in einer Methode implementieren muss, finden Sie im Beispielprojekt nur die Methode awakeFromNib. Wenn Sie das Projekt nach dieser Erweiterung starten, zeigt Ihnen die App ein Zifferblatt mit drei Zeigern an (siehe Abbildung 3.16).

Storyboards, XIB- und NIB-Dateien

Der Name awakeFromNib rührt von NIB-Dateien her, in denen sich ebenfalls Views und andere serialisierte Objekte ablegen lassen. Storyboards hat Apple erst mit iOS 5 eingeführt, und so gibt es noch sehr viele Projekte, die auf NIBs basieren. Bei iOS-Projekten erzeugt Xcode dabei die binären NIB-Dateien für die App aus XIB-Dateien, die die serialisierten Daten im XML-Format enthalten. Obwohl Storyboards NIBs immer stärker verdrängen, gibt es immer noch Anwendungsfälle, an denen XIBs bzw. NIBs sehr praktisch sind. Wir kommen darauf noch zu sprechen.

In den meisten Fällen verhält sich Cocoa Touch bei Storyboards und NIBs gleich, so dass Sie in der Regel die Beschreibungen für Storyboards auch auf NIBs übertragen können. Da kompilierte Storyboards auch aus NIBs bestehen, ist das auch kein Wunder.

Abbildung

Abbildung 3.16 Das Zifferblatt mit Zeigern


Rheinwerk Computing - Zum Seitenanfang

3.1.4Aktualisierung der ZeitanzeigeZur nächsten ÜberschriftZur vorigen Überschrift

Als Nächstes soll die Zeitanzeige kontinuierlich aktualisiert werden. Diese Aufgabe kann entweder der View selbst oder der Viewcontroller übernehmen. Da jedoch jedes Objekt der Klasse ClockView diese Aufgabe erfüllen sollte, wird der dafür nötige Code in der Viewklasse angesiedelt.

Für die Aktualisierung der Zeitanzeige müssen Sie nur in regelmäßigen Abständen den Wert der Property time aktualisieren. Wenn Sie eine kurze Aufgabe wiederholt ausführen möchten, können Sie dafür die Klasse NSTimer verwenden.

Die Klasse ClockView erhält dazu eine weitere Property von diesem Typ und jeweils eine Methode zum Starten und zum Stoppen der Aktualisierung.

Anonyme Kategorie

Die Klasse verwendet die Property timer nur intern. Eine Zugriffsmöglichkeit von außen ist also nicht notwendig. Das lässt sich durch eine anonyme Kategorie vermeiden. Bei einer anonymen Kategorie geben Sie bei der Deklaration zwischen den runden Klammern keinen Namen an, und als Implementierungsteil verwenden Sie den der Klasse.

In der Regel deklarieren Sie die anonyme Kategorie einer Klasse in deren Implementierungsdatei. Sie können die Timer-Property also in der anonymen Kategorie deklarieren, da sie für andere Klassen nicht sichtbar sein soll, weshalb wir solche Propertys auch als privat bezeichnen.

@interface ClockView()

@property(nonatomic, strong) NSTimer *timer;

@end

@implementation ClockView
...
@end

Die neueren Xcode-Versionen legen übrigens bei Viewcontroller-Klassen automatisch jeweils eine anonyme Kategorie an. Bei der Klasse ClockView müssen Sie das hingegen selbst machen.

Sie können so die Property vor dem allgemeinen Zugriff verstecken. In der anonymen Kategorie können Sie sämtliche Methoden und Propertys deklarieren, die die Klasse ClockView nur intern braucht und die sie nicht für die Öffentlichkeit zur Verfügung stellt. Die Verwaltung des Timers erfolgt über die beiden Methoden startAnimation und stopAnimation, auf die auch andere Klassen zugreifen dürfen.

@interface ClockView : UIView
...
- (void)startAnimation;
– (void)stopAnimation;

@end

Listing 3.16 Methoden für die automatische Aktualisierung der Zeitanzeige

Ein Timer ruft eine Methode in einem Objekt nach einer festen Zeitspanne auf, entweder einmal oder auch kontinuierlich. In der Methode startAnimation müssen Sie also ein Timer-Objekt erzeugen und starten, wogegen Sie es in stopAnimation anhalten und zerstören. Sie können über die Property für den Timer außerdem verhindern, dass die App den Timer versehentlich mehrmals startet. Dazu prüft die Methode in Listing 3.17 über die Bedingung self.timer == nil, ob die Property timer nicht schon etwa auf einen Timer verweist:

- (void)startAnimation {
if(self.timer == nil) {
self.timer = [NSTimer
scheduledTimerWithTimeInterval:0.5
target:self selector:@selector(updateTime:)
userInfo:nil repeats:YES];
}
}
- (void)stopAnimation {
[self.timer invalidate];
self.timer = nil;
}

Listing 3.17 Starten und Stoppen der Animation

Der Aufruf von scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: in der dritten Zeile erzeugt und startet einen kontinuierlichen Timer und weist ihn der Property timer zu. Dabei geben der zweite und der dritte Parameter an, dass der Timer die Methode updateTime: im aktuellen Objekt, dem Clockview, aufrufen soll. Der Doppelpunkt im Selektor ist wichtig, da er anzeigt, dass diese Methode einen Parameter erwartet. Das Zeitintervall beträgt eine halbe Sekunde, damit der Timer die Anzeige zweimal pro Sekunde aktualisiert. Da die Methodenaufrufe nicht in exakten Zeitabständen erfolgen, könnten bei längeren Zeitintervallen (z. B. einer Sekunde) sonst Sprünge auftreten, was zu ruckelnden Zeigerbewegungen führt.

Die Methode updateTime: aktualisiert den Wert der Property time. Damit Cocoa Touch den View danach auch neu zeichnet, müssen Sie auch noch die Methode setNeedsDisplay aufrufen. Sie zeichnet den View allerdings nicht direkt neu, sondern markiert ihn nur zum Neuzeichnen. Das erledigt das Betriebssystem erst dann, wenn es dafür Zeit hat.

- (void)updateTime:(NSTimer *)inTimer {
self.time = [NSDate date];
[self setNeedsDisplay];
}

Listing 3.18 Aktualisierung der Zeitanzeige

Automatische Aktualisierung des Views versus Animationen

Die in diesem Abschnitt vorgestellten Methoden dienen zur automatischen Aktualisierung eines Views. Dieses Vorgehen ist für diesen Anwendungsfall auch durchaus ausreichend, da Sie die Zeiger nur zweimal pro Sekunde neu zeichnen müssen. Bei jeder Aktualisierung zeichnet der View nicht nur die Zeiger, sondern auch das komplette Ziffernblatt neu. Die Uhr läuft trotzdem flüssig.

Dieses Vorgehen eignet sich allerdings nicht für beliebige Animationen, da dort in der Regel häufigere Aktualisierungen des Views notwendig sind. Außerdem gibt Ihnen Cocoa Touch dafür mit Core Animation ein viel besseres System an die Hand, und einfache Animationen lassen sich außerdem problemlos über Klassenmethoden von UIView realisieren. Mehr dazu finden Sie in Kapitel 6, »Models, Layer, Animationen«.

Sie können die Aktualisierung der Zeitanzeige testen, indem Sie die Methode startAnimation aus der Methode awakeFromNib heraus aufrufen, und tatsächlich bewegen sich dann auch die Zeiger. Für einen Test ist das auch in völlig in Ordnung. In der fertigen App sollte jedoch lieber der Viewcontroller die Animation steuern, da er genau weiß, wann die App den View anzeigt und verschwinden lässt.

Der View sollte auf jeden Fall vor seiner Zerstörung den Timer stoppen, da dieser ansonsten weiterläuft und fröhlich die Methode updateTime: weiter aufruft. Da die App jedoch inzwischen den View zerstört hat, stürzt sie dann ab. Sie sollten also immer in der Methode dealloc die Methode stopAnimation aufrufen:

- (void)dealloc {
[self stopAnimation];
}

Listing 3.19 Timer bei Zerstörung des Views stoppen


Rheinwerk Computing - Zum Seitenanfang

3.1.5Wiederverwendbarkeit von ViewsZur nächsten ÜberschriftZur vorigen Überschrift

Die Klasse ClockView weist bis jetzt keine Abhängigkeiten vom Viewcontroller oder vom Application-Delegate auf. Sie ist also vollkommen unabhängig von der Controller- oder sogar der Modellschicht des Programms, unsere App hat ja schließlich noch nicht einmal eine Modellschicht. Das ist so gewollt und soll auch so bleiben. Die Unabhängigkeit erlaubt es Ihnen, beliebig viele Uhren in Ihrem Projekt anzulegen und anzuzeigen.

Sie können also erneut das Storyboard öffnen und weitere Clockviews, wie oben beschrieben, anlegen. Sie brauchen dazu einfach nur im Interface-Builder weitere UIView-Objekte auf den Hauptview zu ziehen und die Klasse der neuen Objekte auf ClockView zu setzen. Sie können sogar die Header- und die Implementierungsdatei der Klasse in ein anderes Projekt übertragen und den View dort ohne Einschränkungen oder weitere notwendige Schritte verwenden. Die Größen der Uhren lassen sich über den Größeninspektor ebenfalls beliebig anpassen. Sie können sogar Ziffernblätter erzeugen, die nicht kreis-, sondern ellipsenförmig sind. Letztere sehen allerdings etwas gewöhnungsbedürftig aus. Außerdem können Sie die Farbe für den Hintergrund hinter dem Ziffernblatt frei wählen. Ein Beispiel dafür sehen Sie in Abbildung 3.17.

Abbildung

Abbildung 3.17 Wiederverwendung eines Views

Allerdings sehen die kleineren Uhren nicht besonders schön aus. Das liegt daran, dass wir für die Linienbreiten und den Radius der Minuteneinteilungspunkte absolute Werte verwendet haben. Wenn wir stattdessen relative Werte benutzen, passen sich diese Breiten ebenfalls an die Größe des Views an. Beispielsweise ist die Breite des Stundenzeigers 8. Bei einer Viewbreite von 320 hat die Variable theRadius den Wert 160, und 8 ist ein Zwanzigstel davon. Analog dazu ist 4 ein Vierzigstel und 2 ein Achtzigstel. Wenn Sie also jeweils

CGContextSetLineWidth(theContext, 8.0);
CGContextSetLineWidth(theContext, 4.0);
CGContextSetLineWidth(theContext, 2.0);

und

CGContextAddArc(theContext,thePoint.x, thePoint.y, 4.0, 
0.0, 2 * M_PI, YES);

durch

CGContextSetLineWidth(theContext, theRadius / 20.0);
CGContextSetLineWidth(theContext, theRadius / 40.0);
CGContextSetLineWidth(theContext, theRadius / 80.0);

und

CGContextAddArc(theContext,thePoint.x, thePoint.y, theRadius / 40.0, 
0.0, 2 * M_PI, YES);

ersetzen, passen sich die Breiten und Radien an die Breite des Views an. Das Ergebnis sehen Sie in Abbildung 3.18.

Abbildung

Abbildung 3.18 Anpassung der Linienbreiten an die Viewbreite

Views sollten immer unabhängig von der Controller- und der Modellschicht der App sein. Dadurch erreichen Sie in der Regel einen sehr hohen Wiederverwendungsgrad, und der Code lässt sich dadurch viel besser pflegen. Wie gut das möglich ist, sehen Sie an den vielen Viewklassen des UIKits, die auch alle weder Controller- noch Modellschicht kennen.

Zugegebenermaßen ist das bei der Klasse ClockView auch relativ einfach, da dieser View auch nur eine Uhrzeit darstellen kann. Sie lernen im Verlauf dieses Buches noch weitere Möglichkeiten kennen, wie der View mit den Objekten aus den anderen Schichten interagieren kann, ohne dass dabei Abhängigkeiten zu Klassen aus diesen Schichten entstehen.


Rheinwerk Computing - Zum Seitenanfang

3.1.6Zeichenfarbe festlegenZur vorigen Überschrift

Die Zeichenfarbe für die Zifferblatteinteilung ist bislang fest im Code verdrahtet. Mit iOS 7 hat Apple eine Möglichkeit geschaffen, die Zeichenfarbe für Views einheitlich über die Property tintColor festzulegen. Das Besondere daran ist, dass Sie diese Farbe nicht für jeden View einzeln festlegen müssen, sondern sie auch von jeweils ihren Superview(s) erben können.

Die Klasse ClockView soll den Wert der Property tintColor für die Farbe der Einteilungsstriche des Zifferblatts verwenden. Das lässt sich relativ einfach umsetzen. Die Property besitzt zwar die Klasse UIColor, die Core Graphics nicht kennt; Sie erhalten jedoch über die Property CGColor das entsprechende Core-Graphics-Farbobjekt zu einer Farbe der Klasse UIColor. Damit können Sie die Linien- und die Füllfarbe in der Methode drawRect: folgendermaßen setzen:

UIColor *theColor = self.tintColor;

CGContextSetStrokeColorWithColor(theContext, theColor.CGColor);
CGContextSetFillColorWithColor(theContext, theColor.CGColor);

Listing 3.20 Tint-Color für die Einteilungsstriche verwenden

Durch diese Änderung stellt das Programm nun die Einteilungsstriche in Blau dar. Allerdings können Sie jetzt die Klasse ClockView nicht mehr in Programmen einsetzen, die auch unter den Vorgängerversionen von iOS 7 laufen sollen. Dieses Problem können Sie jedoch beheben, indem Sie die Existenz der Methode tintColor überprüfen, was Sie über die Methode respondsToSelector: durchführen können. In Listing 3.21 sehen Sie die Code-Zeilen, die Sie dafür zusätzlich in die Methode drawRect: aus Listing 3.9 einfügen müssen.

CGContextAddEllipseInRect(theContext, theBounds);
CGContextClip(theContext);
if([self respondsToSelector:@selector(tintColor)]) {

UIColor *theColor = self.tintColor;

CGContextSetStrokeColorWithColor(theContext, theColor.CGColor);

CGContextSetFillColorWithColor(theContext, theColor.CGColor);
}
else {

CGContextSetRGBStrokeColor(theContext, 0.25, 0.25, 0.25, 1.0);
CGContextSetRGBFillColor(theContext, 0.25, 0.25, 0.25, 1.0);
}

CGContextSetLineWidth(theContext, 8.0);
CGContextSetLineCap(theContext, kCGLineCapRound);

Listing 3.21 Optionale Verwendung der Tint-Color

Wenn die App die Tint-Color ändert, muss der Clockview das Zifferblatt neu zeichnen. Um dieses Ereignis mitzubekommen, gibt es die Methode tintColorDidChange. Sie können diese Methode in der Klasse ClockView überschreiben und darin das Neuzeichnen des Views veranlassen:

- (void)tintColorDidChange {
[super tintColorDidChange];
[self setNeedsDisplay];
}

Listing 3.22 Neuzeichnen nach Änderung der Tint-Color



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.

>> Zum Feedback-Formular
<< zurück




Copyright © Rheinwerk Verlag GmbH, Bonn 2014
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de


  Zum Katalog
Zum Katalog: Apps programmieren für iPhone und iPad






Apps programmieren für iPhone und iPad
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Katalog: Apps programmieren für iPhone und iPad






Apps programmieren für iPhone und iPad


Zum Katalog: Einstieg in Objective-C 2.0 und Cocoa






Einstieg in Objective-C 2.0 und Cocoa


Zum Katalog: Spieleprogrammierung mit Android Studio






Spieleprogrammierung mit Android Studio


Zum Katalog: Android 5






Android 5


Zum Katalog: iPhone und iPad-Apps entwickeln






iPhone und iPad-Apps entwickeln


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo