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 5 Daten, Tabellen und Controller
Pfeil 5.1 Benachrichtigungen
Pfeil 5.1.1 Benachrichtigungen empfangen
Pfeil 5.1.2 Eigene Benachrichtigungen verschicken
Pfeil 5.2 Layoutanpassungen und Viewrotationen
Pfeil 5.2.1 Lang lebe das Rotationsprinzip!
Pfeil 5.2.2 Anpassung des Layouts
Pfeil 5.3 Core Data
Pfeil 5.3.1 Datenmodellierung
Pfeil 5.3.2 Implementierung von Entitätstypen
Pfeil 5.3.3 Dokumentenordner und Application-Support-Verzeichnis
Pfeil 5.3.4 Einbindung von Core Data
Pfeil 5.3.5 Der Objektkontext
Pfeil 5.3.6 Die Nachrichten des Objektkontexts
Pfeil 5.3.7 Anlegen und Ändern von Entitäten in der Praxis
Pfeil 5.4 Texte, Bilder und Töne verwalten
Pfeil 5.4.1 Viewanpassungen für die Systemleisten
Pfeil 5.4.2 Die Tastatur betritt die Bühne
Pfeil 5.4.3 Fotos aufnehmen
Pfeil 5.4.4 Töne aufnehmen und abspielen
Pfeil 5.5 Tableviews und Core Data
Pfeil 5.5.1 Tableviews
Pfeil 5.5.2 Tabellenzellen gestalten
Pfeil 5.5.3 Zellprototypen über das Storyboard definieren
Pfeil 5.5.4 Zellprototypen per Programmcode bereitstellen
Pfeil 5.5.5 Der Target-Action-Mechanismus und Tabellenzellen
Pfeil 5.5.6 Zellen löschen
Pfeil 5.6 Core Data II: Die Rückkehr der Objekte
Pfeil 5.6.1 Prädikate
Pfeil 5.6.2 Aktualisierung des Tableviews
Pfeil 5.6.3 Das Delegate des Fetched-Results-Controllers
Pfeil 5.6.4 Tabelleneinträge suchen
Pfeil 5.7 Inhalte teilen
Pfeil 5.7.1 Integration in das Fototagebuch
Pfeil 5.7.2 Eigene Aktivitäten bereitstellen
Pfeil 5.8 Collectionviews
Pfeil 5.8.1 Der Collectionviewcontroller
Pfeil 5.8.2 Gitterdarstellung
Pfeil 5.8.3 Zellen und die Datenquelle
Pfeil 5.8.4 Ergänzende Views
Pfeil 5.8.5 Freie Layouts
Pfeil 5.8.6 Freie Layouts und ergänzende Views
Pfeil 5.8.7 Decorationviews

Rheinwerk Computing - Zum Seitenanfang

5.4Texte, Bilder und Töne verwaltenZur nächsten Überschrift

Als Nächstes implementieren Sie das Verhalten der Views des Diary-Entry-Viewcontrollers.


Rheinwerk Computing - Zum Seitenanfang

5.4.1Viewanpassungen für die SystemleistenZur nächsten ÜberschriftZur vorigen Überschrift

Cocoa Touch verwendet ab iOS 7 auch die Flächen hinter der Status-, Navigations- und Werkzeugleiste für die Darstellung des Views des Viewcontrollers, wodurch diese Systemleisten Teile des Views verdecken (siehe Abbildung 5.10) und der Nutzer den dahinterliegenden Text nicht mehr lesen kann.

Es gibt mehrere Möglichkeiten, dieses Verhalten abzuschalten. Über den Attributinspektor des Viewcontrollers können Sie die Erweiterungsränder über das Feld Extend Edges ausschalten (siehe Abbildung 5.11). Die Erweiterungsränder legen fest, ob der Viewcontroller die Fläche unter den Systemleisten wie Navigations-, Werkzeug- oder Statusleiste sowie die Tabbar nutzen soll, um den View anzuzeigen. Über die Schalter Under Top Bars und Under Bottom Bars können Sie die Bereiche unter den jeweiligen Systemleisten am oberen beziehungsweise unteren Bildschirmrand für die Anzeige freigeben.

Abbildung

Abbildung 5.11 Erweiterungsränder ausschalten

Mit dem dritten Schalter, Under Opaque Bars, legen Sie übrigens fest, ob der Viewcontroller bei eingeschalteten Erweiterungsrändern seinen View auch unter opake – also nicht-transparente – Systemleisten legen soll. Standardmäßig verwendet iOS 7 nur transparente Systemleisten, und Sie können eine Erweiterung des Views und eine Systemleiste auch dadurch verhindern, indem Sie im Attributinspektor der Leiste den Schalter Translucent ausschalten (siehe Abbildung 5.12).

Abbildung

Abbildung 5.12 Transparenz der Werkzeugleiste ausschalten

Bei der Verwendung von Autolayout können Sie Views auch relativ zum unteren Rand der oberen beziehungsweise oberen Rand der unteren Systemleisten positionieren, wofür der Top- beziehungsweise Bottom-Layout-Guide dienen. Diese beiden Elemente enthalten jeweils die Gesamthöhe der oberen beziehungsweise unteren Systemleisten. Sie stellen die entsprechenden Restriktionen im Interface Builder ein, indem Sie den betreffenden View unter- beziehungsweise oberhalb der entsprechenden Systemleisten platzieren und im Pin-Dialog wie in Abbildung 5.13 den Eintrag Top Layout Guide oder Bottom Layout Guide auswählen.

Abbildung

Abbildung 5.13 Restriktion abhängig vom Top-Layout-Guide festlegen

Bei der Verwendung von Autosizing anstelle von Autolayout kann der Viewcontroller die Höhe der Systemleisten berechnen. Die Methode visibleBounds aus Listing 5.23 berechnet das Rechteck, das zwischen den Systemleisten liegt, über die Propertys topLayoutGuide und bottomLayoutGuide, die es allerdings erst seit iOS 7 gibt.

- (CGRect)visibleBounds {
CGRect theBounds = self.view.bounds;

if([self respondsToSelector:@selector(topLayoutGuide)] &&
[self respondsToSelector:@selector(bottomLayoutGuide)]) {
theBounds.origin.y = [self.topLayoutGuide length];
theBounds.size.height -= [self.topLayoutGuide length]
+ [self.bottomLayoutGuide length];
}
return theBounds;
}

Listing 5.23 Berechnung des sichtbaren Rechtecks

Mit dieser Lösung sehen Sie zwar das komplette Bild und den Text, allerdings hat insbesondere der Text-View dann eine sehr kleine Fläche. Aus diesem Grund verwendet der Viewcontroller eine andere Möglichkeit: Er blendet die Navigations- und Werkzeugleiste nur temporär ein, wie Sie das aus der App Fotos von Apple kennen. Dadurch ist genügend Platz für die Anzeige des Fotos und des Textes vorhanden (siehe Abbildung 5.14).

Abbildung

Abbildung 5.14 Tagebucheintrag ohne Navigations- und Werkzeugleiste

Die Grundfunktionen für diesen Ansatz stellt die Kategorie UINavigationController(BarManagement) zur Verfügung. Die Methoden setBarsHidden: und setBarsHidden:animated: erlauben das Anzeigen beziehungsweise Verstecken der Systemleisten, und über barsHidden erfragen Sie den aktuellen Zustand.

- (BOOL)barsHidden {
return self.navigationBarHidden && self.toolbarHidden;
}

- (void)setBarsHidden:(BOOL)inHidden {
[self setBarsHidden:inHidden animated:NO];
}

- (void)setBarsHidden:(BOOL)inHidden animated:(BOOL)inAnimated {
[self cancelHideBars];
[self setNavigationBarHidden:inHidden
animated:inAnimated];
[self setToolbarHidden:inHidden animated:inAnimated];
}

Listing 5.24 Verwaltung der Systemleisten des Navigationcontrollers

Außerdem stellt die Kategorie über die Methode hideBarsWithDelay: eine Möglichkeit bereit, die Systemleisten zeitverzögert auszublenden, und während der Verzögerung lässt sich diese Operation über die Methode cancelHideBars abbrechen.

- (void)hideBarsWithDelay:(NSTimeInterval)inDelay {
[self performSelector:@selector(hideBars)
withObject:nil afterDelay:inDelay];
}

- (void)cancelHideBars {
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(hideBars) object:nil];
}

- (void)hideBars {
[self setBarsHidden:YES animated:YES];
}

Listing 5.25 Verzögertes Ausblenden der Systemleisten

Der Diary-Entry-Viewcontroller nutzt nun diese Methoden, um die Anzeige der Systemleisten zu steuern. Wenn er erscheint, zeigt er sie zunächst ohne Animation an, und er startet das verzögerte Ausblenden. Bei seinem Verschwinden lässt er die Leisten wieder erscheinen, da ja die anderen Viewcontroller in der Regel von sichtbaren Systemleisten ausgehen.

- (void)viewWillAppear:(BOOL)inAnimated {
[super viewWillAppear:inAnimated];
self.navigationController.barsHidden = NO;
}

- (void)viewDidAppear:(BOOL)inAnimated {
[super viewDidAppear:inAnimated];
[self.navigationController hideBarsWithDelay:2.0];
}

- (void)viewWillDisappear:(BOOL)inAnimated {
[self.navigationController setBarsHidden:NO animated:YES];
[super viewWillDisappear:inAnimated];
}

Listing 5.26 Steuerung der Anzeige der Systemleisten

Außerdem kann der Nutzer die Anzeige der Systemleisten durch eine Berührung des Views umschalten. Dazu legen Sie in den Hauptview des Viewcontrollers einen Tap-Gesture-Recognizer, der die Methode toggleBars: aus Listing 5.27 auslöst.

- (IBAction)toggleBars:(id)inSender {
UINavigationController *theController =
self.navigationController;
BOOL theHiddenFlag = theController.barsHidden;

[theController setBarsHidden:!theHiddenFlag animated:YES];
if(theHiddenFlag) {
[theController hideBarsWithDelay:2.0];
}
}

Listing 5.27 Anzeige der Systemleisten über Gesture-Recognizer steuern

Die Methode schaltet die Sichtbarkeit der Systemleisten um, und wenn sie die Leisten eingeschaltet hat, startet sie die verzögerte Ausblendung. Der Gesture-Recognizer soll die Methode jedoch nicht aufrufen, wenn der Nutzer den Text-View berührt. Dazu können Sie dessen Delegate-Methode gestureRecognizerShouldBegin: implementieren, die den Berührungspunkt relativ zum Text-View berechnet und über die Funktion CGRectContainsPoint prüft, ob sich dieser Punkt im Text-View befindet.

- (BOOL)gestureRecognizerShouldBegin: 
(UIGestureRecognizer *)inRecognizer {
UIView *theView = self.textView;
CGPoint thePoint = [inRecognizer locationInView:theView];

return !CGRectContainsPoint(theView.bounds, thePoint);
}

Listing 5.28 Aktivierung des Recognizers nur außerhalb des Text-Views


Rheinwerk Computing - Zum Seitenanfang

5.4.2Die Tastatur betritt die BühneZur nächsten ÜberschriftZur vorigen Überschrift

Im Gegensatz zu Computern oder den Smartphones anderer Hersteller besitzen die iOS-Geräte keine Hardware-Tastatur. Stattdessen zeigt das Gerät eine Tastatur auf dem Bildschirm an, und der Touchscreen empfängt die Tastendrücke. Bei diesem Vorgehen kann das Problem entstehen, dass die Tastatur das Eingabefeld verdeckt. Die Lösung dafür ist eine Verschiebung des Eingabefelds in einen Bereich, den die Tastatur nicht verdeckt.

Für Texteingaben bietet Cocoa Touch die Klassen UITextField und UITextView an. Während Textfelder für die Eingabe von kurzen, meist einzeiligen Texten gedacht sind, eignen sich Text-Views besonders für mehrzeilige und längere Texte. Das Verhalten beider Klassen ist sehr ähnlich, wobei jedoch UITextView im Gegensatz zu UITextField keine Unterklasse von UIControl ist und somit auch keine Ereignisse versendet. Die Tastatur verhält sich bei beiden Klassen hingegen gleich. Wenn Sie ein Textfeld oder einen Text-View antippen, fährt die Tastatur vom unteren Bildschirmrand aus.

Das Fototagebuch verwendet für die Texteingabe einen Text-View, da die Beschreibung eines Eintrags durchaus ein längerer Text sein darf. Allerdings liegt der Text-View so, dass ihn die Tastatur im Hochformat ganz und im Querformat teilweise verdeckt. Der Viewcontroller sollte also dafür sorgen, dass sich der Text-View beim Erscheinen der Tastatur an den freien Bereich des Bildschirms anpasst.

Dazu müssen Sie zunächst mitbekommen, wann Sie die Ausmaße des Views verändern müssen. Dafür haben Sie zwei Möglichkeiten: Sie können entweder die Methoden textViewShouldBeginEditing: beziehungsweise textViewDidEndEditing: des Text-View-Delegates verwenden, oder Sie arbeiten mit Benachrichtigungen. Die Verwendung der Delegate-Methoden ist naheliegend und findet sich in vielen Beispielprogrammen im Web. Allerdings beruht dieser Weg leider auf einem Denkfehler: Die beiden Methoden informieren Sie über den Beginn beziehungsweise das Ende des Editiervorgangs; sie informieren Sie hingegen nicht über das Erscheinen oder Verschwinden der Tastatur. Das ist keine Spitzfindigkeit, denn auf einem iPad mit angeschlossener Hardware-Tastatur zeigt Cocoa Touch die Software-Tastatur nicht an.

Simulation der Hardware-Tastatur

Sie können das Verhalten Ihrer Apps mit einer angeschlossenen Hardware-Tastatur im Simulator testen. Dazu müssen Sie den Menüpunkt HardwareHardware-Tastatur simulieren einschalten. Der Simulator zeigt dann keine Tastatur an, wenn Sie auf ein Texteingabeelement klicken.

Cocoa Touch kann Sie jedoch auch über das Erscheinen und Verschwinden der Tastatur informieren. Das geschieht für das Erscheinen der Tastatur über die beiden Benachrichtigungen UIKeyboardWillShowNotification und UIKeyboardDidShowNotification, und entsprechend informieren UIKeyboardWillHideNotification und UIKeyboardDidShowNotification Sie über das Verschwinden der Tastatur.

Benachrichtigungen auch mit Hardware-Tastatur

Diese Benachrichtigungen verschickt iOS in bestimmten Fällen auch bei angeschlossener Hardware-Tastatur, und zwar dann, wenn das Eingabefeld einen Input-Accessory-View verwendet; das ist in Abbildung 5.15 die Werkzeugleiste mit dem Sichern- und Abbrechen-Button. Damit können Sie zusätzliche Eingabemöglichkeiten auf der Tastatur bereitstellen. Da diese natürlich nicht auf der Hardware-Tastatur vorhanden sind, zeigt Cocoa Touch diesen zusätzlichen View bei angeschlossener Hardware-Tastatur auf dem Display an. Sie sollten also auch bei einer angeschlossenen Tastatur darauf achten, dass auch der Input-Accessory-View nicht die Texteingabe verdeckt.

Abbildung

Abbildung 5.15 Tastatur mit Input-Accessory-View

Um die Änderung der Tastaturanzeige mitzubekommen, registriert sich die Klasse DiaryEntryViewController für zwei Benachrichtigungen in der Methode viewDidAppear:

NSNotificationCenter *theCenter = 
[NSNotificationCenter defaultCenter];

[theCenter addObserver:self
selector:@selector(keyboardWillAppear:)
name:UIKeyboardWillShowNotification object:nil];
[theCenter addObserver:self
selector:@selector(keyboardWillDisappear:)
name:UIKeyboardWillHideNotification object:nil];

Listing 5.29 Registrierung für Tastaturbenachrichtigungen

Die Benachrichtigung liefert im User-Info-Dictionary mehrere Informationen. Über den Schlüssel UIKeyboardFrameEndUserInfoKey können Sie die Koordinaten und die Größe der Tastatur relativ zu den Koordinaten des Fensters ermitteln. Da die Struktur CGRect jedoch keine Objective-C-Klasse ist, kann ein Dictionary diese Werte nicht direkt aufnehmen. Cocoa Touch verpackt sie deswegen in ein Objekt der Klasse NSValue, und über deren Methode CGRectValue erhalten Sie das Rechteck.

Die Methode keyboardWillAppear: rechnet zunächst dieses Rechteck in das Koordinatensystem des Hauptviews um. Aus diesem Rechteck berechnet sie dann das freie Rechteck oberhalb der Tastatur. Das ist genau der Platz, den der Text-View während des Editiervorgangs belegen soll. Sie müssen dieses Rechteck noch in das Koordinatensystem des Superviews des Text-Views umrechnen, da ja dessen Frame relativ dazu ist.

- (void)keyboardWillAppear:(NSNotification *)inNotification {
NSValue *theValue = inNotification.userInfo
[UIKeyboardFrameEndUserInfoKey];
NSNumber *theDuration = inNotification.userInfo
[UIKeyboardAnimationDurationUserInfoKey];
UIView *theView = self.view;
CGRect theFrame = [theView.window convertRect:
CGFloat theY;

[self.navigationController setBarsHidden:YES
animated:YES];
theY = CGRectGetMinY(self.visibleBounds);
theFrame = CGRectMake(0.0, theY,
CGRectGetWidth(self.view.frame),
CGRectGetMinY(theFrame) – theY);
theFrame = [theView convertRect:theFrame
toView:self.textView.superview];
[UIView animateWithDuration:[theDuration doubleValue]
animations:^{
self.textView.frame = theFrame;
}];
}

Listing 5.30 Koordinatenanpassung beim Erscheinen der Tastatur

Zur Konvertierung eines Rechtecks von dem Koordinatensystem eines Views in das Koordinatensystem eines anderen können Sie die Methode convertRect:toView: verwenden, die das Rechteck im ersten Parameter aus dem Koordinatensystem des Empfängers in das Koordinatensystem des Views im zweiten Parameter umrechnet. Bei dieser Umrechnung bleibt die absolute Lage des Rechtecks gleich. Das heißt, wenn Sie die Rechtecke [theValue CGRectValue] im Fenster und das Ergebnisrechteck im View theView zeichnen würden, lägen beide exakt aufeinander. Die Methode rechnet alle Verschiebungen, Drehungen und Skalierungen entsprechend um.

Abbildung 5.16 zeigt die möglichen Anzeigezustände der Tastatur und des Accessory-Views. Im ausgeblendeten Zustand 1 befinden sich sowohl die Tastatur als auch der Accessory-View unterhalb des Fensters im nicht sichtbaren Bereich. Ohne Hardware-Tastatur schiebt Cocoa Touch den Accessory-View und die Software-Tastatur in den sichtbaren Bereich 2. Mit angeschlossener Hardware-Tastatur zeigt Cocoa Touch nur den Accessory-View an 3.

Abbildung

Abbildung 5.16 Die möglichen Anzeigezustände der Tastatur

Das Listing verwendet für die Berechnung die Y-Koordinate des Tastaturrechtecks (in Abbildung 5.16 ist es durch einen schwarzen Punkt dargestellt) und nicht dessen Höhe. Wenn eine Hardware-Tastatur angeschlossen ist, zeigt Cocoa Touch die Software-Tastatur nicht an. Das Rechteck hat indes trotzdem die volle Größe – also Input-Accessory-View und Software-Tastatur. Der View wird also nur so weit hochgeschoben, dass nur der Input-Accessory-View zu sehen ist.

Listing 5.30 zeigt die Koordinatenberechnung. Darin weist die letzte Anweisung das berechnete Rechteck dem Text-View als neuen Frame zu. Diese Zuweisung befindet sich in einem Animationsblock für die Methode animatedWithDuration:animations:.

Mit diesem Konstrukt können Sie sehr einfach implizite Animationen erzeugen. Sie brauchen dabei nur den neuen Wert einer animierbaren Property in einem Animationsblock zu setzen. Cocoa Touch führt dann die notwendigen Schritte für die Animation automatisch durch, indem es zwischen dem alten und dem neuen Wert der Property interpoliert. Kapitel 6, »Models, Layer, Animationen«, geht noch genauer auf Animationen ein.

Die Dauer für die Animation liefert Ihnen die Benachrichtigung unter dem Schlüssel UIKeyboardAnimationDurationUserInfoKey im User-Info-Dictionary auch mit. Die Methode keyboardWillAppear: liest sie aus und verwendet sie als Animationsdauer für die Größenänderung.

Die Tastatur verschwindet

Wenn iOS die Tastatur vom Bildschirm entfernt, soll der Text-View wieder seine alte Position zugewiesen bekommen. Dieses Rechteck können Sie allerdings nicht über das Tastaturrechteck berechnen. In vielen Fällen müssen Sie sich die ursprünglichen Koordinaten merken. Im Beispielprogramm haben die Ränder des Text-Views feste Abstände zu den Rändern seines Superviews. Die Methode keyboardWillDisappear: macht sich diesen Umstand zunutze und berechnet aus den Bounds des Superviews den Frame des Text-Views:

- (void)keyboardWillDisappear:(NSNotification *)inNotification {
NSNumber *theDuration = inNotification.userInfo
[UIKeyboardAnimationDurationUserInfoKey];
[self.navigationController setBarsHidden:YES
animated:YES];
[UIView animateWithDuration:[theDuration doubleValue]
animations:^{
self.textView.frame = CGRectInset(
self.textView.superview.bounds, 10.0, 10.0);
}];
}

Listing 5.31 Koordinatenanpassung beim Verschwinden der Tastatur

Dazu verwendet die Methode die Funktion CGRectInset, die aus dem angegebenen Rechteck und einem horizontalen sowie vertikalen Abstand ein neues Rechteck berechnet. Es liegt innerhalb des ursprünglichen Rechtecks und hat zu diesem jeweils die gewünschten horizontalen und vertikalen Abstände.

Sie können jetzt beim Ausblenden der Tastatur die ursprünglichen Koordinaten des Text-Views wiederherstellen. Aber wie bringen Sie Cocoa Touch dazu, die Tastatur verschwinden zu lassen? Textfelder und Text-Views besitzen dafür kein Standardeingabeelement. Viele Apps verwenden dafür die ¢-Taste mit der Beschriftung Done.

Das Beispielprogramm verwendet hierfür den bereits erwähnten Input-Accessory-View, da sich hierdurch mehrere Möglichkeiten bieten. Diesen Hilfs-View zeigt Cocoa Touch immer oberhalb der Tastatur an. Das Fototagebuch benutzt dafür eine Werkzeugleiste; Sie können jedoch auch beliebige andere Views verwenden.

Einen Input-Accessory-View erstellen Sie getrennt von der View-Hierarchie und weisen ihn der Property inputAccessoryView des Textfelds oder Text-Views zu. In der Szene des Diary-Entry-ViewControllers im Storyboard finden Sie auf der obersten Ebene einen View der Klasse UIToolBar, der als Input-Accessory-View dient. Um ihn mit dem Text-View zu verbinden, können Sie die Werkzeugleiste über eine Outlet-Property mit dem Viewcontroller verbinden und ihn in der Methode viewDidLoad des Controllers der Property inputAccessoryView des Text-Views zuweisen.

Durch einen kleinen Trick lässt sich dieses zusätzliche Outlet indes sparen, denn die Property inputAccessoryView ließe sich ja mit der Werkzeugleiste schon im Interface Builder verbinden, wenn sie eine Outlet-Property wäre. Diesen Mangel können Sie jedoch relativ einfach durch eine Kategorie beheben. Das Beispielprojekt enthält dafür die Kategoriedeklaration UITextView(Outlets) in der Datei UITextView+Outlets.h (siehe Listing 5.32).

@interface UITextView(Outlets)
@property(readwrite, retain) IBOutlet
UIView *inputAccessoryView;
@end

Listing 5.32 Nachträgliche Deklaration eines Outlets

Eine Implementierung dieser Kategorie ist nicht notwendig, da die Klasse ja bereits diese Property implementiert. Falls Sie die Kategorie dagegen erweitern möchten und eine Implementierung für andere Methoden brauchen, sollten Sie die Property dort über @dynamic inputAccessoryView definieren, damit Sie keine Compiler-Warnungen erhalten. Wenn Sie jetzt den Verbindungsinspektor des Text-Views im Interface Builder öffnen, finden Sie dort auch ein Outlet für den Input-Accessory-View. Der Interface Builder erkennt diese Deklaration automatisch und erweitert den Verbindungsinspektor entsprechend; Sie müssen dazu noch nicht einmal die Headerdatei einbinden.

Die Werkzeugleiste enthält drei Elemente: jeweils einen Button zum Sichern und Abbrechen sowie einen unsichtbaren Zwischenraum. Der Zwischenraum sorgt dafür, dass die Werkzeugleiste den Abbrechen-Button an den rechten Rand drückt. Die Buttons sind mit den Action-Methoden saveText: beziehungsweise revertText: verbunden. Beide Methoden lassen dabei die Tastatur über den Aufruf der Methode endEditing: verschwinden.

- (IBAction)saveText:(id)inSender {
[self.view endEditing:YES];
self.diaryEntry.text = self.textView.text;
[self saveDiaryEntry];
}

- (IBAction)revertText:(id)inSender {
[self.view endEditing:YES];
self.textView.text = self.diaryEntry.text;
}

Listing 5.33 Methoden zum Beenden der Texteingabe

Die Methode saveText: weist den Inhalt des Text-Views der Property text des Tagebucheintrags zu und speichert ihn über die Methode saveDiaryEntry. Wenn Sie hingegen die Texteingabe abbrechen, stellt die Methode den ursprünglichen Text im Text-View aus dem Text des Tagebucheintrags wieder her.


Rheinwerk Computing - Zum Seitenanfang

5.4.3Fotos aufnehmenZur nächsten ÜberschriftZur vorigen Überschrift

Neben einem Text kann ein Tagebucheintrag auch ein Bild enthalten. Der Eintrag enthält jedoch die Bilddaten nicht in einem Attribut. Er verweist über die Relationship media auf weitere Entitäten, die Bilder, Töne oder auch andere Medieninhalte aufnehmen können. Die Entitäten des Typs »Medium« enthalten nicht nur die Binärdaten des Mediums, sondern auch dessen Medientyp.

Die Vorgehensweise, die Mediendaten von den Daten des Eintrags zu trennen, hat einige Vorteile. Der wichtigste ist, dass die Mediendaten nicht automatisch mit den Eintragsdaten geladen werden müssen. Die Bild- und Tondaten belegen selbst in komprimierter Form schnell einige Hundert Kilobyte, was bei einem größeren Tagebuch schnell zu Laufzeit- und Speicherproblemen führt. Außerdem ist diese Struktur flexibel. Sie können relativ einfach neue Medientypen (z. B. für Videos) hinzufügen, ohne das Datenmodell ändern zu müssen. Vielleicht wollen Sie die App auch so verändern, dass sie mehrere Bilder pro Tagebucheintrag speichern kann. Auch das lässt sich über diese Struktur relativ einfach verwirklichen.

In der hier vorgestellten Version gibt es hingegen zu jedem Medientyp nur jeweils ein Medienobjekt pro Eintrag. Deshalb besitzt die Klasse DiaryEntry zwei Methoden, die den Zugriff über den Medientyp erlauben. Die Methode mediumForType: liefert zu einem Medientyp das passende Objekt, während die Methode removeMediumForType: das zum Medientyp passende Objekt löscht. Außerdem gibt es eine Methode zum Hinzufügen und Entfernen eines Mediums. Diese vier Methoden sind dank Core Data mit wenigen Code-Zeilen wie folgt implementiert:

- (Medium *)mediumForType:(NSString *)inType {
for(Medium *theMedium in self.media) {
if([[theMedium type] isEqualToString:inType]) {
return theMedium;
}
}
return nil;
}

- (void)removeMediumForType:(NSString *)inType {
Medium *theMedium = [self mediumForType:inType];

[self removeMedium:theMedium];
}

- (void)addMedium:(Medium *)inMedium {
if(inMedium != nil) {
[self removeMediumForType:inMedium.type];
[self addMediaObject:inMedium];
}
}

- (void)removeMedium:(Medium *)inMedium {
if(inMedium != nil) {
inMedium.diaryEntry = nil;
[self.managedObjectContext deleteObject:inMedium];
}
}

Listing 5.34 Verwaltung der Medien über den Tagebucheintrag

Damit haben Sie die Grundlage für die Verwaltung der Medien eines Tagebucheintrags. Diese Implementierung stellt sicher, dass es immer nur maximal ein Medium eines Typs zu einem Eintrag gibt. Wenn Sie beispielsweise ein zweites Foto zu einem Eintrag über die Methode addMedium: hinzufügen, löscht sie automatisch den vorhandenen Eintrag.

Zugriff auf die Kamera

Damit Sie Bilder in das Tagebuch bekommen, müssen Sie die Kamera des iPhones ansprechen. Das geschieht am einfachsten über die Klasse UIImagePickerController, mit der Sie sowohl Bilder und Filme aufnehmen als auch auf Bilder aus der Bildbibliothek des Gerätes zugreifen können. Diese Klasse ist ein fertiger Viewcontroller, den Sie einfach nach Ihren Wünschen konfigurieren und anzeigen. Außerdem müssen Sie natürlich nach der Aufnahme oder Auswahl das Bild zum Tagebucheintrag hinzufügen.

Der Image-Picker unterstützt drei Eingabequellen:

  • UIImagePickerControllerSourceTypeCamera: die im Gerät eingebauten Kameras
  • UIImagePickerControllerSourceTypeSavedPhotosAlbum: das Fotoalbum mit den Aufnahmen der Kamera – also eine virtuelle Filmpatrone der Kamera. Wenn das Gerät keine Kamera besitzt, zeigt der Controller alle Alben des Geräts an.
  • UIImagePickerControllerSourceTypePhotoLibrary: alle auf dem Gerät vorhandenen Fotoalben inklusive der Filmpatrone

Wenn Sie den Image-Picker verwenden möchten, sollten Sie zuerst prüfen, ob das Gerät die gewünschte Eingabequelle besitzt. Falls Sie diesen Schritt unterlassen, kann der Zugriff auf eine nicht vorhandene Quelle zum Absturz Ihrer App führen. Die Klasse UIImagePickerController stellt für den Test die Klassenmethode isSourceTypeAvailable: bereit. Sie übergeben ihr als Parameter eine der drei oben aufgeführten Konstanten und erhalten YES als Ergebnis, wenn die entsprechende Quelle existiert.

Der Diary-Entry-Viewcontroller überprüft die vorhandenen Eingabequellen des Image-Pickers in der Methode viewDidLoad. In Abhängigkeit vom Ergebnis aktiviert beziehungsweise deaktiviert er die entsprechenden Buttons für die jeweilige Eingabequelle. Diese beiden Anweisungen finden Sie in Listing 5.35, das auch die Anweisungen für die Erzeugung des Image-Pickers enthält. Die Methode setzt außerdem die Property allowsEditing, damit Sie die Größe und den Ausschnitt der Bilder im Picker bearbeiten können.

self.imagePicker = [[UIImagePickerController alloc] init];
self.imagePicker.allowsEditing = YES;
self.imagePicker.modalTransitionStyle =
UIModalTransitionStyleFlipHorizontal;
self.imagePicker.delegate = self;
self.cameraButton.enabled =
[UIImagePickerController isSourceTypeAvailable:
UIImagePickerControllerSourceTypeCamera];
self.photoLibraryButton.enabled =
[UIImagePickerController isSourceTypeAvailable:
UIImagePickerControllerSourceTypePhotoLibrary];

Listing 5.35 Initialisierung des Image-Pickers und Prüfung der Eingabequellen

Auf einem iPhone oder einem iPod müssen Sie den Image-Picker als modalen View anzeigen. Die Methode showPickerWithSourceType: zeigt den Image-Picker an:

- (void)showPickerWithSourceType: 
(UIImagePickerControllerSourceType)inSourceType {
if([UIImagePickerController
isSourceTypeAvailable:inSourceType]) {
self.imagePicker.sourceType = inSourceType;
[self presentViewController:self.imagePicker
animated:YES completion:NULL];
}
}

Listing 5.36 Anzeige des Image-Pickers

Verarbeitung von Aufnahmen

Der Image-Picker bietet für die Verarbeitung der Aufnahmen das Delegateprotokoll UIImagePickerDelegate an. Über die Methode imagePickerControllerDidCancel: teilt Ihnen der Controller mit, dass der Anwender die Aufnahme beziehungsweise Bildauswahl abgebrochen hat. Nach einer erfolgreichen Aufnahme oder Bildauswahl ruft der Image-Picker hingegen die Delegate-Methode imagePickerController:didFinishPickingMediaWithInfo: auf. Dieser Methode übergibt der Picker im zweiten Parameter ein Dictionary, das das Bild und weitere Daten enthält. Für die Einträge im Dictionary gibt es vordefinierte Konstanten:

  • UIImagePickerControllerMediaType ist der Schlüssel des Medientyps der Aufnahme. Dieser Wert entspricht einer der Konstanten kUTTypeImage (Bild) oder kUTTypeMovie (Film).
  • UIImagePickerControllerOriginalImage verweist auf das unbearbeitete Bild der Klasse UIImage der Aufnahme.
  • UIImagePickerControllerMediaMetadata enthält ein weiteres Dictionary mit Metadaten des Bildes.
  • UIImagePickerControllerMediaURL enthält eine URL auf den Eintrag des Fotoalbums, den Sie ausgewählt haben.

Wenn Sie das Bearbeiten der Bilder im Image-Picker erlaubt haben, erhalten Sie noch Einträge über die Ausschnittsauswahl:

  • UIImagePickerControllerEditedImage enthält das bearbeitete Bild der Aufnahme – also den vom Anwender gewählten Ausschnitt.
  • UIImagePickerControllerCropRect enthält ein CGRect in einem NSValue-Objekt mit dem Auswahlrechteck.

Objekte der Klasse UIImage können Sie nicht direkt in Core Data speichern, sondern Sie müssen sie dazu erst serialisieren. Das geht am einfachsten, wenn Sie das Bild in ein bekanntes Format umwandeln. Cocoa Touch stellt dafür die beiden Funktionen UIImageJPEGRepresentation und UIImagePNGRepresentation zur Verfügung, die Bilder in JPEG- beziehungsweise PNG-Daten konvertieren.

Das Fototagebuch liest in der Delegate-Methode das bearbeitete Bild aus und wandelt es mit der Funktion UIImageJPEGRepresentation in JPEG-Daten um. Diese Funktion hat zwei Parameter: Der erste ist das UIImage-Objekt, und der zweite ist ein Fließkommawert zwischen 0 (niedrig) und 1 (hoch), der die Bildqualität nach der JPEG-Komprimierung beschreibt. Die Funktion liefert ein Objekt der Klasse NSData mit den Bilddaten zurück. Diese Objekte können Sie problemlos in Core Data speichern.

Dazu dient die Property data der Klasse Medium. Im Beispielprogramm sorgt die Methode updateMediumData:withMediumType: dafür, dass das Programm die Mediendaten immer richtig aktualisiert. Da die App sie auch für die Speicherung der Audiodaten verwendet, erhält sie als zweiten Parameter den Typ der Daten – also eine der beiden Konstanten kMediumTypeImage oder kMediumTypeAudio:

- (void)updateMediumData:(NSData *)inData 
withMediumType:(NSString *)inType {
if(inData.length == 0) {
[self.item removeMediumForType:inType];
}
else {
Medium *theMedium = [NSEntityDescription
insertNewObjectForEntityForName:@"Medium"
inManagedObjectContext:self.managedObjectContext];

theMedium.type = inType;
theMedium.data = inData;
[self.item addMedium:theMedium];
}
[self saveDiaryEntry];
}

Listing 5.37 Speicherung von Mediendaten

Die Implementierung der Delegate-Methode finden Sie in Listing 5.38. Die Delegate-Methode liest das bearbeitete Bild aus dem Info-Dictionary, aktualisiert die Bildanzeige und sichert die Bilddaten. Außerdem schließt die Methode den Image-Picker.

- (void)imagePickerController: 
(UIImagePickerController *)inPicker
didFinishPickingMediaWithInfo:(NSDictionary *)inInfo {
UIImage *theImage = [inInfo
valueForKey:UIImagePickerControllerEditedImage];
NSData *theData = UIImageJPEGRepresentation(
theImage, 0.8);
CGSize theSize = [theImage sizeToAspectFitInSize:
CGSizeMake(60.0, 60.0)];

[self updateMediumData:theData
withMediumType:kMediumTypeImage];
self.imageView.image = theImage;
theImage = [theImage scaledImageWithSize:theSize];
self.item.icon = UIImageJPEGRepresentation(theImage, 0.8);
[inPicker dismissViewControllerAnimated:YES
completion:NULL];
}

Listing 5.38 Abspeichern der Aufnahme

Die Tabellenansicht des Fototagebuchs stellt die Aufnahmen in einem kleineren Format (60 × 60 Pixel) dar. Die App erzeugt die Bilddaten für dieses Icon, indem sie das aufgenommene Bild entsprechend verkleinert und ebenfalls in JPEG-Daten umwandelt. Da die App das Icon für die Tabellenansicht braucht, legt sie seine Daten jedoch nicht als Medien ab, sondern in der Property icon des Tagebucheintrags.

Die Skalierung des Bildes geschieht über die beiden Methoden sizeToAspectFitInSize: und scaledImageWithSize: der Kategorie UIImage(ImageTools). Die Methode sizeToAspectFitInSize: berechnet die Größe des Icons, so dass das Bild komplett in die angegebene Größe passt, wobei das Seitenverhältnis erhalten bleibt. Wenn Sie bei der Größenänderung eines Bildes das Verhältnis von seiner Breite zur Höhe erhalten, ist das Ergebnis nicht verzerrt – also nicht gestreckt oder gestaucht.

- (CGSize)sizeToAspectFitInSize:(CGSize)inSize {
CGSize theSize = self.size;
CGFloat theWidthFactor = inSize.width / theSize.width;
CGFloat theHeightFactor = inSize.height / theSize.height;
CGFloat theFactor = fmin(theWidthFactor, theHeightFactor);

return CGSizeMake(theSize.width * theFactor,
theSize.height * theFactor);
}

Listing 5.39 Größenberechnung mit festem Seitenverhältnis

Die Methode in Listing 5.39 berechnet jeweils den Skalierungsfaktor, um das Bild auf die Breite beziehungsweise die Höhe der Zielgröße inSize zu bringen. Wenn Sie das Rechteck mit diesen beiden Faktoren skalieren, entstehen zwei neue Rechtecke, von denen eines genau die Breite und das andere genau die Höhe des Zielrechtecks hat. Von diesen beiden Rechtecken passt nun genau das mit dem kleineren Skalierungsfaktor in das Zielrechteck.

Sie sehen ein Beispiel dafür in Abbildung 5.17. Das Seitenverhältnis der Breite des Zielrechtecks zu der Breite des Bildrechtecks ist 3 : 4 (0,75) und analog für die Höhe 1 : 2 (0,5). Wie Sie in der Abbildung sehen, passt das Rechteck mit dem kleineren Skalierungsfaktor 1 : 2 genau in das Zielrechteck.

Abbildung

Abbildung 5.17 Größenberechnung mit festem Seitenverhältnis

Die zweite Methode scaledImageWithSize: der Kategorie berechnet ein skaliertes Bild, das genau die angegebene Größe hat. Dafür verwendet sie Core-Graphics-Funktionen und einen Image-Kontext, den sie über die Funktion UIGraphicsBeginImageContext erzeugt. Sie können auf diesen Kontext die gleichen Operationen anwenden, die Sie bereits im dritten Kapitel kennengelernt haben. Im Gegensatz zum Grafikkontext aus dem dritten Kapitel erfolgen die Ausgaben des Image-Kontexts hingegen in ein Bild und nicht auf dem Bildschirm.

- (UIImage *)scaledImageWithSize:(CGSize)inSize {
CGRect theFrame = CGRectMake(0.0, 0.0,
inSize.width, inSize.height);
UIImage *theImage;

UIGraphicsBeginImageContext(inSize);
[self drawInRect:theFrame];
theImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return theImage;
}

Listing 5.40 Skalierung eines Bildes

Listing 5.40 erstellt einen Image-Kontext in der Größe inSize. Die Funktion liefert die Referenz auf den Kontext leider nicht direkt zurück. Sie können sie jedoch auch hier über die Funktion UIGraphicsGetCurrentContext ermitteln. Core Graphics verwaltet seine Kontexte in einem Stapel, weswegen Sie den Image-Kontext über die Funktion UIGraphicsEndImageContext am Ende der Methode wieder freigeben müssen. Das Bild zeichnen Sie über die Methode drawInRect: der Klasse UIImage in den Grafikkontext. Das Bild des Grafikkontexts erhalten Sie schließlich über die Funktion UIGraphicsGetImageFromCurrentImageContext des UIKits.


Rheinwerk Computing - Zum Seitenanfang

5.4.4Töne aufnehmen und abspielenZur nächsten ÜberschriftZur vorigen Überschrift

Anders als für Bilder und Videos gibt es unter iOS keine grafische Standardkomponente für die Aufnahme von Tönen. Dieser Abschnitt beschreibt eine mögliche Umsetzung eines einfachen Controllers für diesen Zweck. Da dieser Controller nicht den kompletten Bildschirm überdeckt, bindet die App diesen Viewcontroller als Subview ein, wie Sie das bereits in Kapitel 4, »Alles unter Kontrolle«, kennengelernt haben. Dabei stellt die abstrakte Oberklasse SubviewController die benötigten Methoden presentFromViewController:animated: und dismissAnimated: für die Anzeige und das Verschwinden des Viewcontrollers bereit. Die Implementierung dieser beiden Methoden entspricht den Methoden aus Abschnitt 4.7, »Eigene Container- und Subviewcontroller«, weswegen wir hier nicht noch einmal genauer darauf eingehen.

Die Klasse SubviewSegue stellt einen passenden Übergang für das Erscheinen eines Subviewcontrollers zur Verfügung. Wegen ihrer engen Verbindung mit der Viewcontroller-Klasse und ihrer sehr kurzen Implementierung befindet sie sich in den Dateien der Viewcontroller-Klasse.

@implementation SubviewSegue

- (void)perform {
[self.destinationViewController presentFromViewController:
self.sourceViewController animated:YES];
}

@end

Listing 5.41 Anzeige eines Subviewcontrollers durch einen Übergang

Xcode 5 erkennt benutzerdefinierte Übergänge übrigens automatisch; wenn Sie eine Verbindung für einen Übergang im Interface Builder ziehen, bietet er Ihnen einen entsprechenden Menüpunkt in der Auswahl an (siehe Abbildung 5.18).

Abbildung

Abbildung 5.18 Benutzerdefinierter Übergang im Interface Builder

Play it again, Sam: Töne abspielen

In Kapitel 3, »Sehen und anfassen«, haben Sie bereits eine Möglichkeit kennenlernt, Töne als Systemsound abzuspielen. Diese Wiedergabemöglichkeit ist allerdings für das Fototagebuch aus mehreren Gründen nicht geeignet. Sie benötigen beispielsweise eine URL, die auf die Sounddatei verweist. Die Töne des Fototagebuchs liegen jedoch in Attributen von Core-Data-Entitäten, für die es keine brauchbaren URLs gibt. Wenn Sie einen Ton über die Funktion AudioServicesPlaySystemSound abspielen, können Sie die Wiedergabe nicht steuern. Sie können die Wiedergabe weder pausieren lassen noch an eine bestimmte Stelle springen. Es ist nur die Wiedergabe des kompletten Tons vom Anfang bis zum Ende möglich.

Über die Klasse AVAudioPlayer des AVFoundation-Frameworks bietet das iOS eine weitere Möglichkeit, Töne wiederzugeben. Diese Klasse erlaubt eine wesentlich flexiblere Wiedergabe von Audiodaten. Zwar ist der Audioplayer auch eine Controller-Komponente, jedoch ist er kein Viewcontroller. Das Fototagebuch enthält aus diesem Grund eine Subviewcontroller-Klasse AudioPlayerController, die einen Audioplayer kapselt.

Abbildung

Abbildung 5.19 Der Audioplayer-Controller

Der View des Audioplayer-Controllers zeigt in der Mitte einen Slider an, der die aktuelle Abspielposition wiedergibt, und Sie können über den Slider auch die Abspielposition wählen. Ein Label daneben stellt die genaue Abspielzeit in Sekunden dar. Darüber befindet sich eine grafische Anzeige des aktuellen Lautstärkepegels. Am unteren Rand gibt es noch zwei Buttons, um den Player zu schließen und die Wiedergabe zu starten oder zu unterbrechen. Da die Initialisierung des Audioplayers einige Sekunden benötigen kann, besitzt der View für die Überbrückung der Wartezeit auch eine Aktivitätsanzeige (Activity-Indicator), auf den wir gleich noch genauer eingehen.

Die Initialisierung des Audioplayers erfolgt in der Methode startAudioPlayer, die Sie in Listing 5.42 sehen.

- (void)startAudioPlayer {
NSError *theError = nil;
AVAudioPlayer *thePlayer = [[AVAudioPlayer alloc]
initWithData:self.audioMedium.data error:&theError];

self.audioPlayer = thePlayer;
if(thePlayer == nil) {
NSLog(@"playAudio: %@", theError);
self.loading = NO;
}
else {
thePlayer.delegate = self;
thePlayer.meteringEnabled = YES;
self.time = self.slider.value;
slider.maximumValue = thePlayer.duration;
self.loading = NO;
[self updateTime:nil];
[self startTimer];
[thePlayer play];
}
}

Listing 5.42 Erzeugung und Initialisierung eines Audioplayers

Im Fehlerfall liefert die Initializer-Methode initWithData:error: des Audioplayers im zweiten Parameter einen Verweis auf einen Fehler zurück, und die Variable thePlayer verweist dann auf nil. Im Fehlerfall schreibt die Methode lediglich eine Log-Ausgabe.

Da der Controller den Lautstärkepegel anzeigen soll, müssen Sie über die Property meteringEnabled die Messung dieser Werte einschalten. Die Dauer des Tons können Sie über die Property duration des Audioplayers ermitteln, um damit den maximalen Wert des Sliders zu setzen. Da der Controller sowohl den Slider als auch die Anzeige des Lautstärkepegels regelmäßig aktualisieren muss, startet er dafür einen Timer. Die Aktualisierung dieser Werte erfolgt in der Methode updateTime:.

- (void)updateTime:(NSTimer *)inTimer {
NSTimeInterval theTime = self.audioPlayer.currentTime;
float theValue;

[self.audioPlayer updateMeters];
theValue = [self.audioPlayer averagePowerForChannel:0] –
kMinimalAmplitude;
self.meterView.value = theValue / -kMinimalAmplitude;
self.slider.value = theTime;
[self updateTimeLabel];
}

Listing 5.43 Aktualisierung der Lautstärkepegel- und Positionsanzeige

Der Audioplayer misst den Lautstärkepegel für jeden Kanal getrennt. Um die aktuellen Werte zu erhalten, müssen Sie zuerst die Methode updateMeters aufrufen, bevor Sie den Wert für die einzelnen Kanäle über averagePowerForChannel: abrufen. Der Rückgabewert dieser Methode liegt zwischen –160 und 0, und die Methode updateTime: normalisiert diesen Wert auf das Intervall von 0 bis 1. Dabei hat die Konstante kMinimalAmplitude den Wert –160.

Die Methode drawRect: der Klasse MeterView, die den aktuellen Lautstärkepegel visualisiert, berechnet aus diesem normalisierten Wert die Breite eines Rechtecks. Dieses Rechteck füllt sie über die Methode drawAsPatternInRect: mit einem Bild, das eine LED mit rechtem Rand der Anzeige darstellt. Sie zeichnet dabei das Bild so oft nebeneinander, bis sie die gesamte Fläche bedeckt hat, die das Rechteck theBounds beschreibt.

- (void)drawRect:(CGRect)inRect {
CGRect theBounds = self.bounds;

UIImage *theImage = [UIImage imageNamed:@"meter.png"];
theBounds.size.width *= self.value;
[theImage drawAsPatternInRect:theBounds];
}

Listing 5.44 Zeichnen des Lautstärkepegels in der Klasse »MeterView«

Tipp

Vielleicht sind Ihnen die abgerundeten Ecken der Lautstärkepegelanzeige aufgefallen. Diese Darstellung können Sie relativ elegant über den Layer des Views realisieren, der eine Property cornerRadius besitzt. Außerdem müssen Sie das Clipping einschalten. Die Klasse MeterView setzt diese Werte in der Methode awakeFromNib.

- (void)awakeFromNib {
[super awakeFromNib];
self.layer.cornerRadius = 5;
self.layer.masksToBounds = YES;
}

Im sechsten Kapitel, »Models, Layer, Animationen«, gehen wir noch genauer auf die Möglichkeiten der Layer ein.

Die Methode updateTime: aktualisiert außerdem den Wert des Sliders, um die aktuelle Abspielposition anzuzeigen. Sie liest dazu die Zeit über die Property currentTime aus und setzt diesen Wert in die Property value des Sliders.

Aktivitätsanzeige und länger laufende Operationen

Der Audioplayer benötigt für das Laden und Initialisieren der Audiodaten einige Sekunden Zeit. Der Nutzer sollte in dieser Zeit eine Rückmeldung von der App erhalten, dass sie gerade beschäftigt und nicht etwa eingefroren ist. Dazu verwendet sie eine Aktivitätsanzeige. Das ist ein View der Klasse UIActivityIndicator, die zwölf im Kreis angeordnete graue Striche darstellt. Sie können über die Methode startAnimating eine Animation dieses Views starten, so dass die Striche reihum jeweils die Helligkeit ändern. Das Stoppen der Animation erfolgt über die Methode stopAnimating.

Abbildung

Abbildung 5.20 Audioplayer mit Aktivitätsanzeige

In der Klasse AudioPlayerController erfolgt die Steuerung der Animation über die Property loading, deren Zugriffsmethoden auf dem Animationszustand der Aktivitätsanzeige basieren.

- (BOOL)loading {
return self.activityIndicator.isAnimating;
}

- (void)setLoading:(BOOL)inLoading {
if(inLoading) {
[self.activityIndicator startAnimating];
}
else {
[self.activityIndicator stopAnimating];
}
}

Listing 5.45 Steuerung der Aktivitätsanzeige

Wenn Sie die Property hidesWhenStopped des Activity-Indikators auf YES setzen, zeigt Cocoa Touch den Indikator nur während der Animation an. Einen Indikator ohne Animation versteckt Cocoa also automatisch.

Die Animation beginnt allerdings nicht sofort durch den Aufruf der Methode startAnimating, sondern erst, nachdem die Programmausführung in die Runloop des Threads zurückgekehrt ist. Das können Sie dadurch erreichen, dass Ihr Programm alle Methoden verlässt, die Sie erstellt haben.

Das bedeutet für den Audioplayer, dass die Ausführung alle Ihre Methoden verlassen muss, bevor Sie den Audioplayer initialisieren. Das können Sie durch einen verzögerten Aufruf der Methode erreichen:

[self performSelector:@selector(startAudioPlayer) 
withObject:nil afterDelay:0.0];

Animationen und Nebenläufigkeit

Wenn das iPhone parallel zu einer länger laufenden Operation, wie beispielsweise bei der Initialisierung des Audioplayers, eine Animation zeigen soll, dann muss es zwei Dinge gleichzeitig erledigen. Die Verarbeitung von Animationen erfolgt in Cocoa Touch in der Runloop, so dass der folgende Code nicht funktionieren kann:

// So bitte nicht
[self.activityIndicator startAnimating];
// lange laufende Operationen ausführen
[self.activityIndicator stopAnimating];

Wenn Sie die Aktivitätsanzeige so verwenden, friert die Anzeige zunächst ein, zeigt dann kurz den laufenden Indikator und lässt ihn anschließend verschwinden.

Die Methode perfomSelector:withObject:afterDelay: stellt die Klasse NSObject bereit. Sie ruft die Methode mit dem angegebenen Selektor und dem Parameterobjekt im Empfänger nach der angegebenen Verzögerung auf. Diese Methode fügt den Methodenaufruf immer in die Runloop ein – auch bei einer Verzögerung von 0 Sekunden. Cocoa Touch bekommt so die Möglichkeit, die Animation der Aktivitätsanzeige zu aktivieren.

Der Start einer Animation mit dem Ausführen einer längeren Aufgabe und das anschließende Stoppen der Animation sehen also folgendermaßen aus:

- (void)startLongTask {
[self.activityIndicator startAnimating];
[self performSelector:@selector(longTask)
withObject:nil afterDelay:0.0];
}

- (void)longTask {
// lang laufende Operation ausführen
[self.activityIndicator stopAnimation];
}

Listing 5.46 Nebenläufige Operation parallel mit Animation ausführen

Die Rückkehr in die Runloop hat indes noch einen weiteren Nebeneffekt: Da sie den Autoreleasepool verwaltet, veranlasst sie ihn, bei der Rückkehr an alle enthaltenen Objekte ein release zu schicken. Objekte, die nur der Autoreleasepool hält, erleben also den Aufruf der verzögerten Methode nicht. Um die Speicherverwaltung zu vereinfachen, sendet die Methode performSelector:withObject:afterDelay: automatisch ein retain an den Nachrichtenempfänger und das Parameterobjekt. Nach der Ausführung der verzögerten Methode bekommen diese beiden Objekte ein release geschickt. Für die Parameter können Sie also ruhig Objekte im Autoreleasepool verwenden.

Animationen und Zeichnen

Cocoa Touch führt alle Zeichen- und Animationsoperationen über die Runloop aus. Diese Methoden markieren die Objekte für die entsprechende Operation. Wenn die Reihenfolge von diesen Operationen im Zusammenhang mit anderen Operationen wichtig ist, müssen Sie also zwischendurch die Runloop zum Zuge kommen lassen. Dazu können Sie im einfachsten Fall eine Struktur wie in Listing 5.46 verwenden. Die Methode performSelector:withObject: eignet sich hierfür allerdings nicht, da sie die Methode mit der übergebenen Signatur direkt ausführt.

Denken Sie an den Doppelpunkt im Selektor, wenn Sie einen Parameter an die Methode mit der lange laufenden Operation übergeben wollen.

Wenn die Operation mehr als 3 bis 5 Sekunden dauern kann, sollten Sie sie in einem eigenen Thread ausführen, um den Hauptthread nicht zu blockieren. Eine einfache Möglichkeit, eine Methode in einem Hintergrundthread auszuführen, bietet die Methode performSelectorInBackground:withObject: der Klasse NSObject. Auch diese Methode hält den Empfänger und das Parameterobjekt im Speicher, und die aufgerufene Methode muss immer einen eigenen Autoreleasepool für die Speicherverwaltung bereitstellen.

Wenn Sie aus dieser Methode Änderungen am View vornehmen möchten, sollten Sie diese Operationen allerdings im Hauptthread ausführen. Sie können dazu die Methode performSelectorOnMainThread:withObject:waitUntilDone: verwenden.

Tipp

Sie können über die performSelector-Varianten auch Methoden ohne Parameter aufrufen. Verwenden Sie dazu einfach den Selektor der Methode und den Parameterwert nil. Wenn Sie mehrere Parameterwerte übergeben möchten, können Sie sie in ein Dictionary einfügen und es als Parameter übergeben. Die Methode kann dann die einzelnen Parameterwerte aus dem Dictionary wieder extrahieren.

- (void)startVeryLongTask {
NSDictionary *theParameters = @{
@"date":[NSDate date], @"date", ..., nil};
[self.activityIndicator startAnimating];
[self performSelectorInBackground:@selector(veryLongTask:)
withObject:theParameters];
}

- (void)veryLongTask:(NSDictionary *)inParameters {
@autoreleasepool {
NSDate *theDate = inParameters[@"date"];
...
[self.textLabel performSelectorOnMainThread:
@selector(setText:) withObject:theText
waitUntilDone:NO];
...
[self.activityIndicator
performSelectorOnMainThread:
@selector(stopAnimating) withObject:nil
waitUntilDone:NO];
}
}

Listing 5.47 Starten eines Hintergrundtasks

Listing 5.47 stellt die Ausführung einer Methode in einem eigenen Thread schematisch dar. Der Hintergrundtask führt die Methode veryLongTask: aus, die den notwendigen Autoreleasepool zur Verfügung stellt.

Die Übergabe der Parameter erfolgt über ein Dictionary, das die Methode startVeryLongTask entsprechend verpackt. Die Methode veryLongTask: kann dann die Parameter aus diesem Dictionary wieder auslesen. Die Methode aktualisiert außerdem ein Label und stoppt die Aktivitätsanzeige, bevor sie endet. Diese beiden Aufrufe führt sie über den Hauptthread durch, da sie die Anzeige aktualisieren.

Achtung, Aufnahme: Töne aufzeichnen

Auch für Tonaufnahmen gibt es eine fertige Controller-Komponente in Cocoa Touch. Das ist die Klasse AVAudioRecorder aus dem AVFoundation-Framework. Wie der Audioplayer ist auch der Audiorecorder kein Viewcontroller, und Sie müssen eine Bedienungsoberfläche erstellen. Das Fototagebuch besitzt dafür einen Subviewcontroller der Klasse AudioRecorderController für Audioaufnahmen, dessen View dem View des Audioplayer-Controllers ähnelt.

Da Sie bei einer Aufnahme nicht die Aufnahmeposition festlegen können, verwendet der Recorder einen Fortschrittsbalken anstatt eines Sliders. Der Fortschrittsbalken zeigt Ihnen während der Aufnahme den Anteil der verbrauchten Aufnahmezeit an. Damit die Audiodaten nicht zu groß werden können, begrenzt der Controller die maximale Aufnahmezeit auf 30 Sekunden, die die Konstante kMaximalRecordingTime festlegt. Sie brauchen also nur den Wert dieser Konstanten zu verändern, um eine andere maximale Aufnahmezeit festzulegen.

1, 2, 3 – hallo, hallo – 1, 2, 3 ...

Bevor Sie eine Audioaufnahme unter iOS starten, sollten Sie überprüfen, ob das Gerät dazu auch in der Lage ist. Dazu stellt das Singleton AVAudioSession die Methode inputIsAvailable bereit. Im Beispielprojekt sperrt der Diary-Entry-Viewcontroller den Button für den Aufruf des Recorders, wenn keine Audioaufnahme möglich ist. Das geschieht über die folgende Zuweisung in viewDidLoad:

self.recordButton.enabled = 
[[AVAudioSession sharedInstance] inputIsAvailable];

Abbildung

Abbildung 5.21 Der Audiorecorder-Controller

Der Controller erzeugt den Audiorecorder in der Methode startRecording. Sie müssen bei der Erzeugung eine URL auf eine Datei im Dateisystem angeben, in die der Recorder die aufgenommenen Daten schreibt.

- (void)startRecorder {
NSError *theError = nil;
AVAudioRecorder *theRecorder = [[AVAudioRecorder alloc]
initWithURL:self.fileURL
settings:self.audioRecorderSettings error:&theError];

if(theRecorder == nil) {
NSLog(@"startRecording: %@", theError);
self.preparing = NO;
}
else {
theRecorder.delegate = self;
self.audioRecorder = theRecorder;
if([self.audioRecorder
recordForDuration:kMaximalRecordingTime]) {
[self updateRecordButton];
[self startTimer];
self.preparing = NO;
}
}
}

Listing 5.48 Anlegen und Initialisieren des Audiorecorders

Da Ihre App unter iOS in einer Sandbox läuft, können Sie die Datei nicht einfach irgendwo in das Dateisystem des Geräts schreiben. Sie müssen erst ein Verzeichnis ermitteln, in das Sie die Datei schreiben können. Da Sie die Audiodatei nicht länger speichern müssen, eignet sich hierfür das Verzeichnis für temporäre Dateien, dessen Pfad Ihnen die Funktion NSTemporaryDirectory aus dem Foundation-Framework liefert.

- (NSString *)filePath {
NSString *theDirectory = NSTemporaryDirectory();

return [theDirectory stringByAppendingPathComponent:
@"recording.caf"];
}

- (NSURL *)fileURL {
return [NSURL fileURLWithPath:self.filePath];
}

Listing 5.49 URL-Bestimmung für die Audiodatei

Dateiablage unter iOS

Sie können keine Dateien in das Ressourcenverzeichnis Ihrer App schreiben. Das funktioniert zwar im Simulator, unter iOS ist dieser Ordner hingegen geschützt, und der schreibende Zugriff führt zu einem Programmfehler.

Sie müssen dem Audiorecorder bei der Initialisierung außerdem das Audioformat beschreiben, in dem Sie aufzeichnen möchten. Hier gibt es eine Fülle von möglichen Parametern, die sich wahrscheinlich nicht alle sinnvoll in einer Methodensignatur unterbringen lassen. Aus diesem Grund bekommt der Initialisierer diese Parameter in einem Dictionary, das die Methode audioRecorderSettings erzeugt. Das Fototagebuch nimmt die Töne mit einem Kanal (mono) in dem Format IMA 4:1 ADPCM von Apple mit der Sampling-Rate 16.000 auf, was einen guten Kompromiss zwischen Qualität und Dateigröße darstellt.

- (NSDictionary *)audioRecorderSettings {
return @{ AVFormatIDKey:@(kAudioFormatAppleIMA4),
AVSampleRateKey:@16000.0, AVNumberOfChannelsKey:@1 };
}

Listing 5.50 Erzeugung der Parameter für die Audioaufnahme

Der Recorder hält die Aufnahme entweder nach 30 Sekunden [Anm.: Das ist der Wert der Konstanten kMaximalRecordingTime aus Listing 5.44.] automatisch an oder indem das Programm die Methode pause aufruft. Nach der Aufnahme müssen Sie die Daten aus der Datei in ein NSData-Objekt lesen, wozu Sie den Convenience-Konstruktor dataWithContentsOfURL: verwenden können. Dieses Datenobjekt übergeben Sie dann der bereits besprochenen Methode updateMediumData:withMediumType:, um es an den Tagebucheintrag anzuhängen. Der Aufruf erfolgt in der Delegate-Methode audioRecorder:didRecordToData: im Diary-Entry-Viewcontroller.

Die Aktualisierung der Fortschritts-, der Lautstärkepegel- und der Zeitanzeige erfolgt analog zum Audioplayer über einen Timer.



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