4.3Navigation- und Pop-over-Controller in der Praxis
So viel Controller-Theorie verlangt nach praktischen Beispielen. Dazu erweitern Sie den analogen Wecker um einen Einstellungsdialog. Dabei soll der Wecker drei Einstellungsmöglichkeiten erhalten:
- Der Nutzer kann den Alarmton an- und ausschalten. Ein abgeschalteter Alarmton ist besonders für lärmempfindliche Personen und notorische Zuspätkommer praktisch. Letztere können sich dann zu Recht entschuldigen, sie hätten ja ihren Wecker nicht gehört.
- Über eine weitere Einstellung soll der Nutzer Ziffern auf dem Ziffernblatt einschalten können.
- Minimalisten schätzen die dritte Einstellungsmöglichkeit, mit der Sie die Unterteilung des Ziffernblattes verändern können.
Projektinformation
Den Quellcode des folgenden Beispielprojekts finden Sie auf der DVD unter Code/Apps/iOS6/ExtendedAlarmClock oder im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Auflage_2/Apps/iOS6/ExtendedAlarmClock.
Für die zweite und dritte Einstellungsmöglichkeit müssen Sie die Klasse ClockView erweitern, damit sie diese Anzeigeoptionen auch darstellen kann. Dazu erhält die Klasse zunächst einmal einen Aufzählungstyp für die unterschiedlichen Einteilungen des Zifferblatts und zwei Propertys:
typedef enum {
PartitionOfDialNone = 0,
PartitionOfDialHours,
PartitionOfDialMinutes
} PartitionOfDial;
@interface ClockView : UIView
@property (nonatomic, strong) NSDate *time;
@property (nonatomic, strong, readonly) NSCalendar *calendar;
@property (nonatomic) BOOL showDigits;
@property (nonatomic) PartitionOfDial partitionOfDial;
- (void)startAnimation;
- (void)stopAnimation;
@end
Listing 4.18 Änderungen im Header von »ClockView.h«
Die Änderungen stellt Listing 4.18 hervorgehoben dar. Der Aufzählungstyp PartitionOfDial legt die drei möglichen Unterteilungen (keine, nur Stunden sowie Minuten und Stunden) für eine Uhr fest. Sie können einen Clockview über die Property partitionOfDial entsprechend konfigurieren. Mit der booleschen Property showDigits können Sie die Anzeige der Ziffern steuern. Das Zeichnen der Ziffern übernimmt die neue Methode drawDigits, die Sie vor der Methode drawRect: in die Implementierung der Klasse einfügen. Im Gegensatz zu den anderen Methoden verwendet sie ausschließlich Methoden aus dem UIKit und keine Core-Graphics-Funktionen zum Zeichnen. Das ist für die Darstellung von Text wesentlich einfacher, und Sie dürfen diese beiden Frameworks beim Zeichnen auch ruhig mischen.
- (void)drawDigits {
CGFloat theRadius = CGRectGetWidth(self.bounds) / 2.0;
UIFont *theFont = [UIFont systemFontOfSize:24];
CGRect theFrame;
[[UIColor darkGrayColor] set];
theRadius *= self.partitionOfDial == PartitionOfDialNone ?
0.9 : 0.6;
for(int i = 1; i <= 12; ++i) {
NSString *theText =
[NSString stringWithFormat:@"%d", i];
theFrame.origin = [self pointWithRadius:theRadius
angle:i * M_PI / 6];
theFrame.size = [theText sizeWithFont:theFont];
theFrame = CGRectOffset(theFrame,
-CGRectGetWidth(theFrame) / 2.0,
-CGRectGetHeight(theFrame) / 2.0);
[theText drawInRect:theFrame withFont:theFont];
}
}
Listing 4.19 Zeichnen des Ziffernblatts
Methoden zur Darstellung von Text stellt die Kategorie NSString(UIStringDrawing) des UIKits zur Verfügung. Listing 4.19 verwendet zur Textdarstellung die Methode drawInRect:withFont:. Das Rechteck für die Darstellung berechnen Sie über die Größe des Textes, die Sie über die Methode sizeWithFont: ermitteln. Die bereits bekannte Methode pointWithRadius:angle: liefert den Mittelpunkt des darzustellenden Textes. Da Sie jedoch den Ursprungspunkt des Rechtecks benötigen, müssen Sie ihn durch eine Verschiebung um jeweils die halbe Breite und Höhe des Rechtecks berechnen. Die Verschiebung wird dabei von der Funktion CGRectOffset durchgeführt.
Der Wert der Schleifenvariablen i entspricht übrigens der darzustellenden Ziffer, und aus ihr lässt sich auch der Winkel i × 2π / 12 = i × π / 6 berechnen. Zur Erinnerung noch einmal die Herleitung dieser Formel. Eine Stunde ist ein Zwölftel des Ziffernblattes. Ein Kreis hat 360°, also hat eine Stunde 360° / 12 = 30°. Da das iPhone bekanntlich nur Winkel im Bogenmaß versteht, müssen Sie in dieser Rechnung 360° durch 2π ersetzen und erhalten 2 π / 12 = π / 6 als Winkel für eine Stunde.
Falls das Ziffernblatt keine Unterteilungsstriche anzeigt, verwendet die Methode einen größeren Radius, um die Ziffern weiter außen darzustellen. Außerdem stellt es die Ziffern in Dunkelgrau dar. Dazu setzt das Listing die Textzeichenfarbe über die Anweisung [[UIColor darkGrayColor] set].
Als Nächstes passen Sie die Methode drawRect: der Klasse ClockView entsprechend Listing 4.20 an. Hier sind nur die wesentlichen Zeilen für die Änderungen und jeweils drei Punkte für die restlichen Anweisungen angegeben, um die Übersichtlichkeit zu erleichtern. Für die Konfiguration der Unterteilung müssen Sie zwei Abfragen einfügen. Wenn das Ziffernblatt keine Unterteilung haben soll, braucht das Programm die Schleife nicht zu durchlaufen, und nur für die Darstellung von Minutenunterteilungen darf es den else-Block innerhalb der Schleife ausführen.
- (void)drawRect:(CGRect)inRectangle {
...
if(self.partitionOfDial != PartitionOfDialNone) {
// Zifferneinteilung zeichnen
for(NSInteger i = 0; i < 60; ++i) {
...
if(i % 5 == 0) {
...
}
else if(self.partitionOfDial ==
PartitionOfDialMinutes) {
// Minuteneinteilungsstrich darstellen
...
}
}
}
if(self.showDigits) {
[self drawDigits];
}
[self drawClockHands];
CGContextRestoreGState(theContext);
}
Listing 4.20 Anpassungen an der Methode »drawRect:«
Schließlich fügen Sie am Ende einen Aufruf der neuen Methode drawDigits in einem if-Block ein. Diese Methode darf das Programm schließlich nur ausführen, wenn es Ziffern darstellen soll.
Wenn Sie das Programm jetzt ausführen, sehen Sie einen weißen Kreis mit den Zeigern darauf. Da die Objective-C-Laufzeitumgebung alle Attribute einer Klasse mit 0 initialisiert, sind die Unterteilungsstriche und die Ziffern zunächst einmal ausgeschaltet. Sie sollten also nun Ihre App um einen Dialog erweitern, mit dem der Nutzer diese Einstellungen anpassen kann.
Erweiterung von universellen und lokalisierten Apps
Genauer gesagt müssen Sie vier Einstellungsdialoge erstellen, da Ihre App zwei Gerätefamilien und zwei Sprachen unterstützt. Das klingt nach viel Arbeit, die sich jedoch bei einer systematischen Vorgehensweise relativ gut begrenzen lässt. Erweitern Sie die App zunächst nur für eine Gerätefamilie (am besten für das iPhone) und eine Sprache.
Achten Sie dabei jedoch darauf, dass Ihre Testumgebung (Simulator, iOS-Geräte) auch genau zu dieser Konfiguration passt. Das gilt insbesondere für die Spracheinstellungen. Erst wenn Sie die Anpassungen für diese Konfiguration beendet haben, nehmen Sie die anderen Sprachen und die andere Gerätefamilie (also das iPad) in Angriff – allerdings auch hier immer nur jeweils eine Konfiguration gleichzeitig. Dabei können Sie neue Viewcontroller einfach in die anderen Storyboards kopieren. Das geht auch mit mehreren Viewcontrollern auf einmal, wobei Xcode dann sogar die Übergänge zwischen diesen Controllern mitkopiert.
4.3.1Navigationcontroller anlegen

Die Wecker-App braucht also einen Einstellungsdialog, den sie über einen Navigationcontroller
anzeigt. Dazu erweitern Sie zunächst das Storyboard für iPhones. Da die App den Einstellungsdialog
über einen Navigationcontroller anzeigt, ziehen Sie zunächst aus der Bibliothek einen
solchen Controller auf die Zeichenfläche des Storyboards. Der Interface Builder legt
dabei automatisch einen neuen Rootviewcontroller an, den Sie jedoch nicht brauchen.
Selektieren Sie dieses Objekt, und löschen Sie es über die Taste oder
. Als Nächstes ziehen Sie eine Verbindung vom Navigation- zum vorhandenen Viewcontroller
des Weckers und wählen im darauf erscheinenden Pop-over-Dialog den Punkt root view controller aus (siehe Abbildung 4.16). Sie haben dem Navigationcontroller nun einen neuen Rootviewcontroller zugewiesen.
Der Alarmclock-Viewcontroller sollte jetzt auch eine Navigationsleiste anzeigen. Dort tragen Sie »Alarm Clock« in den Titel des Navigation-Items ein. Durch einen Doppelklick in die Mitte der Leiste können Sie diese Änderungen direkt in der simulierten Navigationsleiste vornehmen. Außerdem legen Sie dort auf der rechten Seite einen Button (Typ UIBarButtonSystemItemCompose) für den Aufruf der Einstellungen an.
Die App sollte nun den Navigationcontroller als initialen Viewcontroller des Fensters verwenden. Dazu öffnen Sie den Attributinspektor des Navigationcontrollers und setzen dort das Häkchen bei Is Initial View Controller. Beim Viewcontroller für die Zeitanzeige entfernen Sie dieses Häkchen. Nach diesen Änderungen sollte Ihr Storyboard so wie in Abbildung 4.25 aussehen.
Abbildung 4.25 Der Navigationcontroller in der Wecker-App
Um Ihre Änderungen zu überprüfen, sollten Sie die App starten. Achten Sie dabei darauf, dass die Spracheinstellungen des Simulators beziehungsweise des Geräts der Sprache des Storyboards entsprechen. Die App sollte nun eine Navigationsleiste mit dem Titel und einem Button anzeigen. Allerdings liegt das Zifferblatt unter der Navigationsleiste, und nur deren Zeiger schimmern sehr stark verschwommen durch (siehe Abbildung 4.26). Seit iOS 7 legt Cocoa Touch die Views der Apps standardmäßig unter die Systemleisten.
Abbildung 4.26 Das Zifferblatt ist teilweise verdeckt.
Sie können diesen Effekt jedoch über den Attributinspektor des Viewcontrollers abschalten, indem Sie alle Checkboxen unter Extend Edges löschen (siehe Abbildung 4.27). Am besten nehmen Sie diese Änderung bei allen Viewcontrollern der Wecker-App vor. Außerdem sollten Sie die Größe des Clock-Views und -Controls noch einmal prüfen.
Durch diese Umstellung sollte der Interface Builder das Zifferblatt nach unten schieben, und in der laufenden App sollte es die Navigationsleiste nicht mehr verdecken.
Abbildung 4.27 Zifferblatt nicht unter der Navigationsleiste einblenden
4.3.2Einen Dialog für die Einstellungen gestalten

Als Nächstes erstellen Sie den Dialog für die Einstellungen der App. Dazu eignen sich hervorragend statische Tableviews, die Apple mit iOS 5 eingeführt hat. Ein Tableview verwaltet seinen Inhalt in untereinanderliegenden Zellen, die er in Abschnitte gruppieren kann. Während die Anzahl der Zellen in einem dynamischen Tableview von der darzustellenden Datenmenge der Applikation abhängt, ist sie bei statischen Tableviews fest. Dynamische Tableviews lernen Sie in Kapitel 5, »Daten, Tabellen und Controller«, kennen.
Um statische Tableviews nutzen zu können, müssen Sie einen Tableviewcontroller verwenden. Ziehen Sie ein solches Objekt aus der Bibliothek auf die Zeichenfläche Ihres Storyboards (siehe Abbildung 4.28).
Der Tableviewcontroller enthält bereits einen Tableview, den Sie allerdings zunächst noch konfigurieren müssen, wozu Sie den Attributinspektor des Tableviews öffnen (siehe Abbildung 4.29). Wählen Sie unter Content den Punkt Static Cells aus, um einen statischen Tableview zu erhalten. Der Tableview soll sich in zwei Abschnitte aufteilen, weswegen Sie unter Sections den Wert 2 einstellen. Wenn Sie unter Style den Eintrag Grouped auswählen, zeigt der Tableview die Abschnittstitel in breiten grauen Balken an und zeichnet keine Striche zwischen den Zellen. Außerdem sollten Sie die Auswahl von Zellen unterbinden, indem Sie unter Selection den Punkt No Selection auswählen.
Abbildung 4.28 Tableviewcontroller anlegen
Als Nächstes können Sie die Zellen des Tableviews gestalten. Im ersten Abschnitt benötigen Sie zwei Zellen und im zweiten Abschnitt eine Zelle. Der Interface Builder hat bereits zwei Abschnitte mit jeweils drei Zellen angelegt. Sie brauchen also nur die überflüssigen Zellen zu löschen. In alle Zellen ziehen Sie jeweils ein Label, deren Texte Sie in »Digits« (Ziffern), »Partition of Dial« (Ziffernblatteinteilung) und »Sound« (Ton) umändern. In die Zellen für das Ein- und Ausschalten der Ziffern und des Tons fügen Sie jeweils auf der rechten Seite noch einen Schalter (UISwitch) ein.
Für die Einstellung der Ziffernblatteinteilung brauchen Sie ein Segmented-Control. Das ist ein Schalter, der mehrere Zustände haben kann. Für die App benötigen Sie drei Zustände (keine Einteilung, Stundeneinteilung sowie Stunden- und Minuteneinteilung). Allerdings reicht dafür der Platz in der Zelle nicht aus. Sie können jedoch die Höhe der Zelle in deren Größeninspektor verändern. Tragen Sie unter Row Height den Wert 92 ein, und setzen Sie den Haken in der danebenliegenden Checkbox Custom, dann lässt sich das Control unter dem Label platzieren.
Abbildung 4.29 Einstellungen für den statischen Tableview
Im Attributinspektor des Segmented-Controls stellen Sie unter Segments drei Abschnitte ein, denen Sie die Beschriftungen »None«, »Hours« und »Minutes« geben. Die Absatzüberschriften können Sie anpassen, indem Sie die Objekte mit der Bezeichnung Table View Section in der Baumdarstellung auswählen. Öffnen Sie jeweils den Attributinspektor der Objekte, und geben Sie unter Header »Appearance« beziehungsweise »Alarm« ein. Der fertige View sollte wie in Abbildung 4.30 aussehen.
Abbildung 4.30 Der Einstellungsdialog des Weckers
Als Nächstes legen Sie den Übergang zum Einstellungsdialog an. Dazu ziehen Sie eine Verbindung vom Button in der Navigationsleiste des Viewcontrollers für die Zeitanzeige zu dem neuen Controller für die Einstellungen. In der Auswahl für den Übergangstyp wählen Sie den Punkt push aus. Durch diese Änderung sollte der Viewcontroller für die Einstellungen auch eine Navigationsleiste anzeigen. [Anm.: Andernfalls sollten Sie diese Leiste über den Attributinspektor des Viewcontrollers unter Top Bar in der Rubrik Simulated Metrics einschalten.] Durch einen Doppelklick in die Mitte dieser Leiste können Sie deren Titel in »Preferences« ändern.
Sie können nun den Einstellungsdialog in Ihrer App aufrufen, indem Sie auf diesen Button tippen. Da Sie bei seinem Vorgänger einen Titel in das Navigation-Item eingetragen haben, zeigt die Navigationsleiste beim Einstellungsdialog auch einen Zurück-Button an, über den Sie zur Zeitanzeige zurückkehren können. Bevor Sie fortfahren, sollten Sie sich durch einen Testlauf im Simulator vergewissern, dass Sie den Einstellungsdialog richtig eingebunden haben.
4.3.3Früher war alles besser ...

Mit der Einführung von iOS 7 hat Apple das Oberflächendesign sehr stark verändert. Der Interface Builder in Xcode 5 zeigt Ihnen alle Elemente im iOS-7-Look an. Wenn Ihre App jedoch auch unter iOS 6.1 und älter laufen soll, können Sie sich über den Assistant-Editor Ihre Views auch im alten Design anzeigen lassen. Dazu wählen Sie im Storyboard den gewünschten Viewcontroller aus, öffnen den Assistant-Editor und öffnen über die Sprungleiste den Menüpunkt Preview (siehe Abbildung 4.31).
Im Assistant-Editor erscheint eine weitere Ansicht des Viewcontrollers; allerdings ebenfalls im iOS-7-Design. Sie können nun zur Darstellung im iOS-6.1-Design wechseln, indem Sie unter der Voransicht auf den Button iOS 7.0 and Later drücken und aus dem Pop-up-Menü den Punkt iOS 6.1 and Earlier auswählen (siehe Abbildung 4.32).
Übrigens übernimmt die Voransicht sofort alle Änderungen, die Sie an dem View im Interface Builder vornehmen. Dadurch können Sie sehr schön ausprobieren und überprüfen, wie sich Änderungen auf das alte und das neue Design auswirken.
Falls die Voransicht fehlt
Diese Voransicht ist nur dann verfügbar, wenn das Storybord das Dateiformat von Xcode 5 verwendet. Sie können das Format über den Dateiinspektor des Storyboards ansehen und ändern.
Abbildung 4.31 Neues und altes Design einträchtig nebeneinander
Abbildung 4.32 Auswahl des Interface-Designs
4.3.4Einstellungen dauerhaft speichern

Die App zeigt den Einstellungsdialog zwar an, und auch die Schalter funktionieren bereits, allerdings hat das noch keinen Einfluss auf die Zeitanzeige. Der Einstellungsdialog benötigt also eine eigene Logik in Form einer eigenen Klasse. Dafür legen Sie im Projekt eine Unterklasse von UITableViewController mit dem Namen PreferencesViewController an, die Sie dem neuen Viewcontroller im Storyboard über dessen Identitätsinspektor zuweisen. Anschließend löschen Sie aus der Implementierungsdatei den kompletten Inhalt des Implementierungsblocks.
In die neue Klasse fügen Sie für jedes der drei Controls eine passende Outlet-Property mit dem Speicherverwaltungstyp weak hinzu, so dass die Deklaration der anonymen Kategorie der Klasse wie in Listing 4.21 aussieht. Am einfachsten legen Sie diese drei Propertys an, indem Sie jeweils eine Verbindung von den Controls in die Kategoriedeklaration des Einstellungscontrollers ziehen und die entsprechenden Namen eingeben.
@interface PreferencesViewController()
@property (weak, nonatomic) IBOutlet UISwitch *digitsSwitch;
@property (weak, nonatomic) IBOutlet
UISegmentedControl *partitionControl;
@property (weak, nonatomic) IBOutlet UISwitch *soundSwitch;
@end
Listing 4.21 Klassendeklaration für den Einstellungsdialog
Natürlich haben Sie durch die Einführung dieser neuen Klasse das Verhalten des Programms noch nicht verändert. Es stellt sich nun die Frage, wie der Einstellungsdialog die Änderungen an den Wecker übergeben soll. Die naive Lösung zu dieser Frage lautet: Der Controller für die Einstellungen besitzt eine Referenz auf den Clockview oder den Viewcontroller für die Zeitanzeige und schreibt die geänderten Werte direkt oder indirekt in den Clockview. Bei dieser Lösung entsteht meist eine starke Abhängigkeit zwischen den Viewcontroller- und/oder Viewklassen.
Um unsere Frage zu beantworten, sollten Sie noch eine weitere, implizite Anforderung berücksichtigen: Die Einstellungswerte sollen bei einem Neustart der App erhalten bleiben. Sie müssen also die Einstellungswerte nicht nur an das Ziffernblatt, sondern auch an eine Komponente übertragen, die diese Werte dauerhaft speichern kann. Mit der Lösung dieser Anforderung erhalten Sie auch eine recht elegante Lösung für das oben beschriebene Abhängigkeitsproblem.
Die Klasse NSUserDefaults aus dem Foundation-Framework erlaubt eine permanente Speicherung nahezu beliebiger Schlüssel-Wert-Paare. Dazu verwenden Sie in der Regel das Objekt, das Ihnen die Klassenmethode standardUserDefaults liefert. Listing 4.22 enthält zwei Methoden, mit denen der Einstellungsdialog seine Werte speichern und wiederherstellen kann.
- (void)savePreferences {
NSUserDefaults *theDefaults =
[NSUserDefaults standardUserDefaults];
[theDefaults setBool:self.digitsSwitch.on
forKey:@"showDigits"];
[theDefaults
setInteger:self.partitionControl.selectedSegmentIndex
forKey:@"partitionOfDial"];
[theDefaults setBool:self.soundSwitch.on
forKey:@"playSound"];
[theDefaults synchronize];
}
- (void)restorePreferences {
NSUserDefaults *theDefaults =
[NSUserDefaults standardUserDefaults];
[theDefaults synchronize];
self.digitsSwitch.on =
[theDefaults boolForKey:@"showDigits"];
self.partitionControl.selectedSegmentIndex =
[theDefaults integerForKey:@"partitionOfDial"];
self.soundSwitch.on =
[theDefaults boolForKey:@"playSound"];
}
Listing 4.22 Wiederherstellung und Speicherung der Einstellungen
Wofür »NSUserDefaults« verwenden?
Sie können zwar in den User-Defaults beliebig große Daten und Datenmengen abspeichern, diese Klasse ist jedoch vornehmlich für die Einstellungsdaten der App und nicht für die Ablage größerer Dokumente oder Daten gedacht. Wenn Sie größere Datenmengen dauerhaft speichern möchten, sollten Sie andere Möglichkeiten, wie das Dateisystem oder das Framework Core Data, nutzen. Das Fototagebuch in Kapitel 5, »Daten, Tabellen und Controller«, zeigt ein Beispiel für die Nutzung von Core Data.
Fügen Sie diese beiden Methoden zu der Implementierung der Klasse PreferencesViewController hinzu, und deklarieren Sie sie auch im Header. Das User-Defaults-Objekt benutzen Sie dabei wie ein Verzeichnis. Sie können jeweils unter einem Schlüssel einen Wert dauerhaft speichern, und diesen Wert können Sie anhand des Schlüssels wiederherstellen. Beispielsweise speichert die Wecker-App die Ziffernblatteinteilung als einen Ganzzahlwert unter dem Schlüssel »partitionOfDial« ab.
Einstellungen synchronisieren
NSUserDefaults legt die Einstellungswerte nicht sofort im Dateisystem des iOS-Gerätes ab, sondern nur in periodischen Zeitabständen. Durch einen Aufruf der Methode synchronize können Sie allerdings die sofortige Speicherung erzwingen. Diese Methode dient auch dazu, die aktuellen Einstellungen aus dem Dateisystem zu laden.
Sie können nun diese beiden Methoden aufrufen, bevor beziehungsweise nachdem die App den Einstellungsdialog anzeigt. Dazu überschreiben Sie die Methoden viewWillAppear: und viewWillDisappear: so wie in Listing 4.23:
- (void)viewWillAppear:(BOOL)inAnimated {
[super viewWillAppear:inAnimated];
[self restorePreferences];
}
- (void)viewWillDisappear:(BOOL)inAnimated {
[self savePreferences];
[super viewWillDisappear:inAnimated];
}
Listing 4.23 Wiederherstellung und Speicherung des Anzeigezustands
Der Einstellungsdialog behält nun seine Werte auch über einen Programmneustart hinweg bei. Das sollten Sie ruhig einmal mit ein paar Testläufen ausprobieren.
Über die Methode registerDefaults: können Sie in den User-Defaults Standardwerte für nicht gesetzte Schlüssel-Wert-Paare festlegen. Dadurch können Sie beispielsweise die Einstellungen vorbelegen, wenn der Nutzer die App das erste Mal aufruft; dabei sollte die App nach dem Start diese Werte so früh wie möglich setzen. Die Wecker-App ruft diese Methode wie in Listing 4.24 in der Methode application:didFinishLaunchingWithOptions: des Application-Delegates auf. Damit Sie dort die Konstante PartitionOfDialMinutes verwenden können, müssen Sie allerdings auch die Datei ClockView.h in die Datei AlarmClockAppDelegate.m importieren.
- (BOOL)application:(UIApplication *)inApplication
didFinishLaunchingWithOptions:(NSDictionary *)inOptions {
NSUserDefaults *theDefaults =
[NSUserDefaults standardUserDefaults];
[theDefaults registerDefaults:@{ @"showDigits": @YES,
@"partitionOfDial" : @(PartitionOfDialMinutes),
@"playSound": @YES }];
return YES;
}
Listing 4.24 Vorbelegung der Einstellungen
Jetzt fehlt nur noch die Übergabe der Werte an die Zeitanzeige. Dafür können Sie genauso wie beim Einstellungsdialog vorgehen, das heißt, der Viewcontroller für die Zeitanzeige liest die Werte aus den User-Defaults und übergibt sie an den View. Diese Aufgabe übernimmt die Methode updateClockView, die Sie in die Implementierung der Klasse AlarmClockViewController einfügen und von der Methode viewWillAppear: aus aufrufen (siehe Listing 4.25). Da Cocoa Touch beim Entfernen des Einstellungsdialogs aus dem Navigationcontroller diese Methode aufruft, übernimmt die Uhr dadurch auch die neuen Einstellungswerte in die Anzeige, und es entsteht auch keine Abhängigkeit zwischen den beiden Viewcontrollern.
- (void)updateClockView {
NSUserDefaults *theDefaults =
[NSUserDefaults standardUserDefaults];
self.clockView.showDigits =
[theDefaults boolForKey:@"showDigits"];
self.clockView.partitionOfDial =
[theDefaults integerForKey:@"partitionOfDial"];
[self.clockView setNeedsDisplay];
}
- (void)viewWillAppear:(BOOL)inAnimated {
[super viewWillAppear:inAnimated];
[self updateClockView];
[self updateViews];
}
Listing 4.25 Aktualisierung der Einstellungen in der Zeitanzeige
Nun können Sie über den Einstellungsdialog das Aussehen der Zeitanzeige verändern, und die App speichert Ihre Einstellungen auch dauerhaft ab. Dabei nutzt die App zur Lösung der Frage nach der Wertübergabe die User-Defaults, was den angenehmen Nebeneffekt hat, dass die App die Einstellungen auch dauerhaft speichert.
Etwas begriffsstutzig ...
... scheint der Clockview zu sein, wenn Sie den Aufruf der Methode setNeedsDisplay in updateClockView weglassen. Dann sieht er nämlich nach dem Verlassen des Einstellungsdialogs zunächst genauso aus wie vorher. Nach einer kurzen Zeit springt dann die Anzeige auf die neuen Werte um. Das Ziffernblatt zeigt die neuen Einstellungen nämlich erst an, wenn der Timer die Zeitanzeige aktualisiert. Durch den Aufruf von setNeedsDisplay erzwingen Sie die Aktualisierung jedoch schon vorher.
Die App wertet jetzt die Einstellungen für die Ziffernblatteinteilung und die Anzeige der Ziffern aus. Über die dritte Einstellung können Sie den Ton für die lokale Benachrichtigung an- oder abschalten. Um diese Einstellungsmöglichkeit zu aktivieren, müssen Sie die Methode createAlarm in AlarmClockViewController ändern und dort die Zuweisung der Property soundName in eine if-Abfrage packen:
if([[NSUserDefaults standardUserDefaults]
boolForKey:@"playSound"]) {
theNotification.soundName = @"ringtone.caf";
}
Listing 4.26 Anpassung, um den Sound abzuschalten
Wenn die App zur Laufzeit eine lokale Benachrichtigung empfängt, spielt sie den Ton ab. Auch hier sollte sie die Einstellungen berücksichtigen. Dazu müssen Sie die Methode playSound im App-Delegate anpassen:
- (void)playSound {
if([[NSUserDefaults standardUserDefaults]
boolForKey:@"playSound"]) {
NSNumber *theId = self.soundId;
if(theId) {
AudioServicesPlaySystemSound(
[theId unsignedIntValue]);
}
}
}
Listing 4.27 Den Ton nur bei der entsprechenden Einstellung abspielen
Das erste Mal
Sie können den ersten Start einer App im Simulator und auf dem Gerät beliebig oft simulieren, indem Sie einfach die App über das Springboard löschen. Halten Sie dazu das App-Icon so lange gedrückt, bis die Icons zu wackeln anfangen. Sie können die App nun über das Kreuz an der linken oberen Ecke des Icons löschen.
Eleganter geht es mit dem bereits erwähnten Programm SimPholders: Damit lässt sich nicht nur die App leichter löschen, Sie können damit auch nur die gespeicherten Daten der App entfernen, was die Durchführung von mehreren Testläufen vereinfacht.
4.3.5Storyboard lokalisieren

Mit diesen Änderungen haben Sie die Umstellung der englischen Version des Programms für das iPhone abgeschlossen. Als Nächstes sollten Sie sich die deutsche Version für das iPhone vornehmen. Da das ursprüngliche Projekt noch kompatibel mit iOS 4 ist, ist dort die Base-Internationalisierung noch ausgeschaltet.
Um sie zu aktivieren, müssen Sie zunächst dem Storyboard eine Sprache zuordnen und daraus dann die Base-Lokalisierung des Storyboards erzeugen. Dazu öffnen Sie zunächst den Dateiinspektor des iPhone-Storyboards und drücken den Button Localize..., den Sie auch in Abbildung 4.33 sehen. Als Sprache wählen Sie im darauffolgenden Dialog Englisch, da Sie ja alle Texte in diesem Storyboard in Englisch verfasst haben.
Sie können auch anders ...
Falls Xcode den Button Localize... nicht anzeigt, sondern stattdessen eine Liste mit einer oder mehreren Sprachen, können Sie den eben beschriebenen Schritt einfach auslassen. Außerdem können Sie statt Englisch auch Deutsch oder eine andere Sprache verwenden. Die folgende Beschreibung geht lediglich davon aus, dass Sie bereits ein englisches Storyboard haben. Die einzelnen Schritte sind jedoch unabhängig von der konkreten Sprache.
Als Nächstes erzeugen Sie die Base-Lokalisierung aus dem Storyboard, indem Sie die Projekteinstellungen öffnen und dort die Checkbox Use Base Internationalization einschalten (siehe Abbildung 4.34).
Abbildung 4.33 Lokalisieren eines Storyboards
Abbildung 4.34 Base-Internationalisierung einschalten
Xcode fragt Sie dann für die einzelnen Dateien, welche Sprache Sie jeweils für die Base-Lokalisierung verwenden möchten; hier sollten Sie wie in Abbildung 4.35 die Auswahl English verwenden.
Abbildung 4.35 Sprachauswahl für Base-Lokalisierung
Nach diesem Schritt enthält der Dateiinspektor des Storyboards unter der Rubrik Localization drei Einträge. Wenn Sie das Häkchen vor German setzen, legt Xcode die Datei AlarmClock.strings unterhalb von AlarmClock.storyboard an, in die Sie die deutschen Texte für die Benutzeroberfläche eintragen. Dabei sollten Sie jedoch nur die Texte in den Anführungszeichen auf der rechten Seite der Gleichheitszeichen ändern, so dass Sie beispielsweise die Zeilen
/* Class = "IBUILabel"; text = "Digits"; ObjectID = "X0u-Xp-hWy"; */
"X0u-Xp-hWy.text" = "Digits";
in
/* Class = "IBUILabel"; text = "Digits"; ObjectID = "X0u-Xp-hWy"; */
"X0u-Xp-hWy.text" = "Ziffern";
umändern. Die Schlüssel auf der linken Seite hat Xcode generiert, und Sie sollten
sie keinesfalls ändern. Über die App Einstellungen beziehungsweise Settings können Sie die Sprache des Simulators umstellen und Ihre App in einer anderen Sprache
testen. Vor dem Test sollten Sie jedoch die generierten Projektdaten über +
+
löschen und die App aus dem Simulator entfernen.
4.3.6Anpassung an das iPad
Die Anpassungen für das iPad können Sie nun analog zu der Erstellung des deutschen Storyboards durchführen, indem Sie den Viewcontroller für die Einstellungen aus dem englischen Storyboard für das iPhone in das Storyboard für das iPad kopieren und einen Navigationcontroller analog zum iPhone-Storyboard anlegen. Achten Sie dabei darauf, den initialen Viewcontroller richtig zu setzen, und vergessen Sie nicht, die Übergänge zwischen den Viewcontrollern anzulegen. Wenn Sie den Einstellungsdialog der iPad-App danach aufrufen, stellen Sie allerdings fest, dass er optisch nicht sehr ansprechend ist. Das iPad zieht den Tableview mit den Einstellungen auf die volle Bildschirmgröße auf, und der Wechsel zwischen den beiden Viewcontrollern sieht sehr unruhig aus, weil Cocoa Touch den kompletten Bildschirmbereich auswechselt. Führen Sie diese Umstellung für die englische Version jedoch trotzdem aus, weil sie die Basis für eine iPad-spezifische Anpassung ist.
Auf dem iPad ist ein Pop-over für die Anzeige des Einstellungsdialogs geeigneter, da Sie hier die Anzeigegröße des Views frei wählen können. Dazu müssen Sie im Interface Builder erst einmal diesen View kleinkriegen. Dazu wählen Sie im Attributinspektor des Einstellungsdialogs unter Size den Punkt Freeform, aktivieren die Option Use Explicit Size und verwenden die Größe 320 × 320 Punkten. Damit der Interface Builder den Tableview in der richtigen Größe anzeigt, stellen Sie in dessen Größeninspektor die gleiche Größe ein.
Als Nächstes ändern Sie den Übergang von der Zeitanzeige zu den Einstellungen von Push in Pop-over um. Selektieren Sie dazu den Übergang im Storyboard, öffnen Sie seinen Attributinspektor, und wählen Sie dort Popover unter Style aus. Dadurch verschwindet die Navigationsleiste aus dem Einstellungsdialog, und nach dieser Änderung öffnet die App die Einstellungen in einem Pop-over (siehe Abbildung 4.36).
Abbildung 4.36 Der Einstellungsdialog als Pop-over
Bei den Anpassungen für das iPad brauchten Sie bis jetzt keine Zeile Code zu schreiben. Die App wählt beim Programmstart das passende Storyboard zur Gerätefamilie aus, und das Storyboard legt dann über den Übergang die Anzeigevariante des Einstellungsdialogs fest.
Allerdings funktioniert die App auf dem iPad noch nicht ganz zufriedenstellend. Wenn Sie den Einstellungsdialog schließen, übernimmt die Zeitanzeige die neuen Werte nicht. Sie müssen dafür die App erst neu starten. Das liegt daran, dass Cocoa Touch die Methode viewWillAppear: beim Schließen des Popups nicht aufruft. Die App zeigt ja das Ziffernblatt auch während der Darstellung des Einstellungsdialogs an, und so ist dieser Methodenaufruf hier nicht sinnvoll. Die Zeitanzeige muss also auf einem anderen Weg die Änderung der Einstellungen mitbekommen.
Dazu eignet sich das Key-Value-Observing, das Sie bereits in Kapitel 2, »Die Reise nach iOS«, kennengelernt haben. Der Viewcontroller für die Zeitanzeige registriert sich dazu in der Methode viewDidAppear: als Beobachter für die Schlüssel showDigits und partitionOfDial der User-Defaults. Entsprechend dazu muss der Viewcontroller die Beobachtung in viewWillDisappear: auch wieder aufheben. Dies ist ein typischer Fall für die in Kapitel 3, »Sehen und Anfassen«, genannte Faustregel für die Ressourcenverwaltung.
Die Beobachtermethode muss nun lediglich die Methode updateClockView aufrufen. Den Programmcode dafür finden Sie in Listing 4.28. Nach dieser Änderung übernimmt das Ziffernblatt die Einstellungen, sobald Sie den Einstellungsdialog schließen.
- (void)viewDidAppear:(BOOL)inAnimated {
[super viewDidAppear:inAnimated];
NSUserDefaults *theDefaults =
[NSUserDefaults standardUserDefaults];
[theDefaults addObserver:self forKeyPath:@"showDigits"
options:0 context:nil];
[theDefaults addObserver:self
forKeyPath:@"partitionOfDial" options:0 context:nil];
[self.clockView startAnimation];
}
- (void)viewWillDisappear:(BOOL)inAnimated {
NSUserDefaults *theDefaults =
[NSUserDefaults standardUserDefaults];
[theDefaults removeObserver:self
forKeyPath:@"showDigits"];
[theDefaults removeObserver:self
forKeyPath:@"partitionOfDial"];
[self.clockView stopAnimation];
[super viewWillDisappear:inAnimated];
}
- (void)observeValueForKeyPath:(NSString *)inKeyPath
ofObject:(id)inObject change:(NSDictionary *)inChange
context:(void *)inContext {
[self updateClockView];
}
Listing 4.28 An- und Abmeldung für das Key-Value-Observing
Für den Nutzer wäre es natürlich hilfreich, wenn er die Auswirkungen der Einstellungsänderungen unmittelbar sehen könnte – wenn also das Ziffernblatt die Einstellungen schon aus dem geöffneten Dialog übernähme. Auch diese Anforderung lässt sich leicht erfüllen. Dazu muss die App lediglich die Einstellungen immer dann in den User-Defaults speichern, wenn der Nutzer eine Einstellung ändert. Durch das Key-Value-Observing leitet der Viewcontroller für die Zeitanzeige die neuen Werte unmittelbar an den Clockview weiter.
Sie können dieses Verhalten erreichen, indem Sie in der Klasse PreferencesViewController eine Action-Methode dafür bereitstellen, die Sie mit den Value-Changed-Events der Controls verbinden. Am einfachsten verwenden Sie dafür die Methode savePreferences, indem Sie ihren Ergebnistyp in IBAction ändern. Wenn Sie nun die App auf einem iPad starten und den Einstellungsdialog aufrufen, übernimmt das Ziffernblatt die neuen Einstellungen, sobald Sie einen Wert im geöffneten Popup-Dialog ändern.
Sie sollten auch das Storyboard für das iPad lokalisieren, indem Sie seinen Dateiinspektor öffnen und den Button Localize... (siehe Abbildung 4.33) drücken. Da Sie das Projekt bereits auf Base Internationalization umgestellt haben, können Sie in der folgenden Dialogbox den Eintrag Base auswählen und so direkt die Base-Lokalisierung des Storyboards anlegen. Von da ab führen Sie die gleichen Schritte wie bei der Lokalisierung des iPhone-Storyboards durch: Anlegen der Strings-Datei über den Dateiinspektor und Übersetzen der darin enthaltenen Texte.
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.