Se si potesse stilare una classifica dei meccanismi meno conosciuti di Cocoa, il Key-Value Observing pattern o KVO sicuramente ricoprirebbe uno dei primi posti. KVO è un meccanismo molto utile che permette agli oggetti di essere notificati rispetto a cambiamenti di specifiche proprietà di altri oggetti. Come è facile immaginare, questo genere di meccanismi si prestano molto bene al pattern di programmazione Model-View-Controller. In quest’articolo cercheremo di capire meglio come usare questa tecnica affiancando la teoria ad un esempio pratico che ne dimostri appieno le funzionalità.
Cosa vogliamo realizzare.
Visto che una delle cose che mi viene chiesta più spesso è la costruzione di un lettore MP3 personalizzato, per l’esempio di quest’articolo ho pensato di sfruttare KVO per costruire un semplice e snello lettore di stream audio per web radio.
In letteratura e sul web esistono tanti esempi di lettori streaming per iOS. Il più famoso di essi è sicuramente quello descritto da Matt Gallagher nel suo articolo “Streaming and
playing an MP3 stream” [http://cocoawithlove.com/2008/09/streaming-and-playinglive-
mp3-stream.html]. Il metodo descritto da Matt nel suo articolo si basa su un complesso meccanismo di download e caching gestito da CFSocket. Sebbene il risultato ottenuto sia egregio, l’inclusione o l’adattamento del progetto a particolari casi d’uso può risultare molto laborioso per via della natura stessa dell’approccio. Nel nostro articolo, invece, sfrutteremo un oggetto nativo di Cocoa: AVPlayer. Il risultato sarà una classe facilmente trasportabile che potremo includere in qualunque progetto, sia esso destinato al mondo iOS o al mondo Mac. Per trarre il massimo profitto da quest’approccio, faremo uso del meccanismo KVO unitamente alla costruzione di un protocollo adhoc.
Prima di andare avanti, proviamo a semplificare con una grafica quello che vogliamo ottenere.
Il nostro oggetto, CPStreamPlayer, dovrà essere in grado di notificare i cambiamento di stato (titolo del brano in riproduzione, buffer in esaurimento, buffer riempito, etc.) all’interfaccia grafica o a qualunque altro oggetto ne faccia richiesta. Per ottenere questo scopo utilizzeremo un pattern di programmazione molto interessante esposto dal framework Cocoa, denominato Key-Value Coding.
KVC è un meccanismo che permette di impostare o leggere un valore di variabile usando il nome stesso. Il nome è una semplice stringa e ci riferiremo ad essa con il termine chiave o key. Ogni oggetto che eredita da NSObject può fare accesso alle sue proprietà attraverso coppie chiave-valore.
Cosa significa?
Immaginate di avere un oggetto che espone una proprietà denominata ‘title’. Per poter configurare a dovere questa proprietà è possibile usare il tradizionale costrutto:
[myObject setTitle: @"qualcosa"];
oppure, seguendo la notazione dotted introdotta da Objective-C 2.0:
myObject.title = @"qualcosa";
esiste un’altro metodo che sfrutta il concetto di Key-Value Coding:
[myObject setValue:@"qualcosa" forKey:@"title"];
Allo stesso modo, se vorremo recuperare il valore della proprietà title potremo usare i costrutti noti di Objective-C, oppure fare ricorso al meccanismo KVC:
NSString *myTitle = [myObject valueForKey:@"title"];
A questo punto dell’articolo potrebbe venire naturale una domanda: perché stiamo introducendo un nuovo modo per impostare e recuperare il valore delle proprietà? Non bastano i metodi già noti?
Per poter rispondere a questa domanda è necessario introdurre un altro concetto che snoccioleremo nel corso dell’articolo: Il meccanismo di Key-Value Observing o KVO. Ogni oggetto che configura le sue proprietà attraverso il meccanismo sopra esposto di KVC, può fare uso del meccanismo di KVO. Questo meccanismo permette di osservare gli eventuali cambiamenti delle proprietà di un oggetto da un’altro oggetto, nell’ipotesi di alterare i valori delle proprietà attraverso il meccanismo di KVC.
NSNotificationCenter e Key-Value Observing
Alcuni di voi avranno già avuto modo di dialogare ed interagire con le notifiche. Questa soluzione coinvolge il centro di notifica o NSNotificationCenter. In questo approccio, il nostro oggetto CPStreamPlayer invierà dei messaggi al centro di notifica che, a sua volta, girerà queste informazioni a tutti gli oggetti che si sono registrati per riceverle.
Ritornando al nostro esempio, sfruttando questo approccio, le entità coinvolte saranno tre: NSNotificationCenter, CPStreamPlayer e gli eventuali Observer.
Nell’approccio con KVO, invece, quello che faremo sarà aggiungere un observer sull’oggetto player, in
maniera da essere notificati direttamente in base a quello che succede dietro le quinte dell’oggetto CPStreamPlayer. In quest’ultimo caso, le entità coinvolte saranno due: CPStreamPlayer ed Observer.
Sebbene entrambi i pattern di programmazione sembrano essere simili, il loro funzionamento a basso livello è molto diverso. Come regola generale di programmazione il pattern con Notification Center dovrebbe essere impiegato solo quando sussiste l’esigenza di notificare uno stesso messaggio a più osservatori assolutamente slegati tra loro. Questi messaggi dovrebbero essere di tipo allarme e tutto l’overhead di trasferire informazioni aggiuntive sarà a carico nostro, attraverso il concetto di NSNotification e dizionario di userInfo (vedremo questo pattern di programmazione in dettaglio in un altro articolo).
Inoltre, utilizzando un’entità esterna (NSNotificationCenter) le comunicazioni tra gli oggetti potrebbero essere ritardate, causando dei piccoli glitch sulla nostra user interface. La comunicazione KVO, invece, non coinvolge altri attori ed è diretta tra i due oggetti che la instaurano. Diremo che due oggetti che fanno KVO tra loro sono legati o “bindati”.
Un ultima considerazione relativamente all’oggetto che cercheremo di costruire nel nostro articolo. Per rendere il nostro esempio il più portabile possibile, costruiremo un oggetto wrapper a più alto livello, che si preoccuperà per noi di dialogare con AVPlayer.
Nella grafica sotto è mostrata una semplificazione di quello che sarà la classe CPStreamPlayer insieme al meccanismo di Key-Value Observing:
KVO in pratica
Ci sono tre passi da effettuare per configurare correttamente un osservatore.
- Prima di tutto, bisogna accertarsi di essere davanti ad uno scenario in cui l’approccio KVO risulti utile. Ad esempio, un oggetto che ha bisogno di essere notificato quando viene effettuata una modifica ad una proprietà di un altro oggetto. Nel nostro caso, l’oggetto CPStreamPlayer dovrà essere a conoscenza dei cambiamenti che avvengono sullo stream attualmente in corso gestito dagli oggetti AVPlayer ed AVPlayerItem.
- L’oggetto CPStreamPlayer deve registrarsi come osservatore sugli oggetti che intende monitorare, inviando un messaggio addObserver:forKeyPath:options:context:
Il messaggio addObserver:forKeyPath:options:context: stabilisce una interconnessione tra gli oggetti che specifichiamo. La connessione non viene specificata tra le due classi, bensì tra le istanza degli oggetti a runtime. Tornando al nostro esempio, l’oggetto wrapper CPStreamPlayer sarà osservatore dei suoi oggetti privati in maniera da poterne monitorare lo stato. - Per rispondere alle notifiche di cambiamento, l’osservatore deve implementare il metodo observeValueForKeyPath:ofObject:change:context:
Questo metodo definisce come l’osservatore risponde alle notifiche di cambiamento. In pratica, è il metodo su cui dobbiamo lavorare per reagire ai cambiamenti che arriveranno.
Il metodo observeValueForKeyPath:ofObject:change:context: viene invocato automaticamente quando una proprietà risulta modificata seguendo lo standard KVO.
Il primo beneficio di quest’approccio consiste sicuramente nel non (re)implementare un nostro personale schema per l’invio di notifiche ogni qualvolta una proprietà dei nostri oggetti verrà cambiata. Tipicamente non ci sono altre azioni oltre a quelle elencate nei quattro passi sopra per adottare il meccanismo di notifica basato su KVO. A differenza del meccanismo di notifica centrale attraverso NSNotificationCenter, inoltre, le notifiche vengono inviate direttamente agli osservatori registrati. NSObject fornisce un’implementazione base per questo meccanismo e, raramente, si avrà l’esigenza di
modicarne l’implementazione.
AVPlayer ed AVItem – Come funzionano gli stream MP3
Dalla versione 4.0, iOS mette a disposizione un oggetto utilissimo per la fruizione di contenuti multimediali, compresi quelli che arrivano da un flusso MP3 trasmesso via http.
L’oggetto in questione si chiama AVPlayer. Esso funziona equamente bene sia con oggetti
locali che con oggetti remoti e/o in stream. E’ possibile anche creare una playlist con oggetti multipli ed avere controllo sui singoli elementi.
L’organizzazione degli oggetti relativamente ad uno stream musicale può essere semplificata nel seguente diagramma:

La prima cosa da fare per poter sfruttare l’oggetto AVPlayer è allocarne un’istanza generica in memoria:
player = [[AVPlayer alloc] init];
Il prossimo passo è quello di creare un’istanza di AVPlayerItem, ovvero un’istanza che conosca cosa vogliamo suonare (file locale o stream remoto). Nel nostro caso, passeremo ad AVPlayerItem un oggetto NSURL, contenente la stringa dell’endpoint http remoto da suonare.
NSURL *streamUrl = [NSURL URLWithString:streamAddress];
AVPlayerItem *anItem = [AVPlayerItem playerItemWithURL:streamUrl];
Una volta ottenuta un’istanza valida di AVPlayerItem possiamo rimpiazzare il currentItem della nostra istanza di AVPlayer, come segue:
[player replaceCurrentItemWithPlayerItem:anItem];
Infine, lanciamo un messaggio di play all’istanza di AVPlayer, per iniziare la riproduzione:
[player play];
E’ necessario spendere qualche altra parola relativamente alle modalità di streaming MP3.
I server di stream MP3, tipicamente, seguono una falsa riga incentrata su un sistema di unicast per ogni singolo client connesso. In pratica, ogni cliente che richiede lo stream avrà riservato un suo spazio per lo streaming ma non potrà comunicare con il server.
Per poter costruire un lettore da zero, sarebbe necessario scendere in dettaglio, relativamente alla codifica dei dati ricevuti e come vengono generati i blocchi (chunks) di informazione. Questa è una della parti più corpose dell’articolo di Matt Gallagher a cui mi riferivo sopra. Visto che noi useremo un oggetto a più alto livello, disponibile nel framework Cocoa/CocoaTouch, non avremo bisogno di scendere a questo livello di dettaglio. Ci basterà conoscere l’indirizzo http dello stream che vogliamo usare.
Risolto questo problema, se ne presenta un’altro non di meno conto. Uno stream di dati ha alcune difficoltà concettuali da risolvere. I dati, infatti, vengono inseriti in un buffer tampone che serve per evitare un’interruzione dell’ascolto in caso di un’improvviso calo di banda nel nostro dispositivo. Questo buffer viene aggiustato dinamicamente in funzione delle condizioni di banda ed il lettore deve adattarsi a queste condizioni dinamiche.
Un altro aspetto che merita attenzione riguarda l’aggiornamento del brano attualmente in ascolto.
Gli attuali sistemi di stream permettono l’invio di un pacchetto ad inizio brano, unitamente ai dati dell’audio, contenente le informazioni relative al brano attualmente in riproduzione.
L’idea è quella di aggiornare la nostra vista soltanto quando se ne verifica una reale esigenza (ad esempio, quando cambia il brano del nostro stream o quando stiamo effettuando il buffering dello stream). E’ in questo contesto che trova spazio quanto esposto relativamente a KVO e KVC. Senza un meccanismo di questo tipo, infatti, l’aggiornamento del brano attualmente riprodotto diventerebbe una sfida senza soluzioni. Se aggiornassimo il titolo ad intervalli regolari, ci troveremmo quasi sempre in una condizione di inconsistenza tra il brano riprodotto e quello visualizzato, stessa cosa per quanto riguarda i messaggi di sistema relativi al buffering. Ancora, utilizzando un paradigma basato sul polling ad intervalli regolari, tutta la user interface ne risentirebbe a livello di fluidità e reattività.
Supponendo che l’istanza di AVPlayerItem da suonare sia denominata anItem, potremo così procedere:
[anItem addObserver:self
forKeyPath:@"timedMetadata"
options:NSKeyValueObservingOptionNew
context:NULL];
In questo modo, la classe CPStreamPlayer (self), sarà notificata relativamente a nuovi cambiamenti (NSKeyValueObservingOptionNew) che intervengono per le proprietà elencate sopra. I più attenti di voi avranno notato che il metodo utilizzato non è quello precedentemente illustrato ma una sua variante denominata addObserver:forKeyPath:options:context: che prende come parametro un keypath
e non una chiave.
Cosa è un Keypath?
Semplicemente è un percorso di chiavi, separate da punti, che esprimono un percorso sino ad arrivare ad una particolare proprietà. Visto che le proprietà che stiamo cercando sono direttamente esposte dall’istanza dell’oggetto AVPlayerItem, nel nostro caso il keypath corrisponderà con la key.
A questo punto l’istanza di CPStreamPlayer sarà legate all’istanza di AVPlayerItem denominata anItem. Questo significa che qualunque cambiamento delle proprietà sopra elencate, scatenerà un messaggio observeValueForKeyPath:ofObject:change:context: sull’oggetto observer CPStreamPlayer.
Ci resta dunque da implementare questo metodo e la relativa logica per reagire ai singoli eventi:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if([keyPath rangeOfString:@"playbackBufferEmpty"].location != NSNotFound && self.isPlaying)
{
[player pause];
}
if([keyPath rangeOfString:@"playbackLikelyToKeepUp"].location != NSNotFound)
{
[player play];
}
else if([keyPath rangeOfString:@"timedMetadata"].location != NSNotFound)
{
if([[change objectForKey:@"new"] isKindOfClass:[NSArray class]])
{
//update songTitle and Artist
}
}
}
Nel caso di playbackBufferEmpty, metteremo in pausa l’istanza del player e proveremo a lanciare un messaggio di CPStreamPlayerDidPaused al delegato designato. Nel caso di playbackLikelyToKeeup, ossia quando il buffer è quasi pieno, ripristineremo l’ascolto.
Infine, nel caso di una variazione alla proprietà timedMetadata, ossia quando variano i metadati associati allo stream, reagiremo popolando correttamente i corrispondenti campi del brano: titolo ed artista.
UIViewController e gestione del player
Ci resta da gestire il controllore di vista, responsabile del player. Anche in questo caso, si tratterà di un’operazione davvero semplice. La classe CPStreamPlayer, implementa un protocollo informale:
@protocol CPStreamPlayerDelegate
@optional
- (void)CPStreamPlayerDidStarted:(CPStreamPlayer *)actPlayer;
- (void)CPStreamPlayerDidPaused:(CPStreamPlayer *)actPlayer;
- (void)CPStreamPlayerMetadataDidUpdated:(CPStreamPlayer *)actPlayer;
@end
La nostra istanza di UIViewController dovrà implementarlo se desidera ricevere notifiche relativamente a start e stop dello stream e cambiamento dei metadati. Nel nostro esempio base, ci basterà aggiornare il titolo del pulsante di play ed i campi che indicano il brano in riproduzione:
#pragma mark -
#pragma mark CPStreamPlayerDelegate
- (void)CPStreamPlayerDidStarted:(CPStreamPlayer *)actPlayer {
[aButton setTitle:@"Buffering" forState:UIControlStateNormal];
}
- (void)CPStreamPlayerDidPaused:(CPStreamPlayer *)actPlayer {
[aButton setTitle:@"Play" forState:UIControlStateNormal];
}
- (void)CPStreamPlayerMetadataDidUpdated:(CPStreamPlayer *)actPlayer {
[aButton setTitle:@"Stop" forState:UIControlStateNormal];
songTitle.text = actPlayer.songTitle;
songArtist.text = actPlayer.artistTitle;
channel.text = actPlayer.channelTitle;
}
Un ultimo tocco: il controllo remoto
iOS ci permette di sfruttare i controlli multimediali disponibili sulla cuffia o quelli che compaiono quando il telefono è bloccato, premendo rapidamente due volte il tasto home.
Per fare questo è necessario impostare il nostro UIViewController come primo risponditore implementando il seguente metodo:
/* required for remote control */
- (BOOL)canBecomeFirstResponder {
return YES;
}
A questo punto, la nostra classe UIViewController è in grado di rispondere agli eventi inviati dal controllore multimediale remoto. Il metodo da implementare per reagire correttamente a questi eventi è remoteControlReceivedWithEvent:. Ci basterà controllare l’oggetto UIEvent passato come parametro e reagire di conseguenza:
#pragma mark -
#pragma mark Remote Control Event Handling
- (void)remoteControlReceivedWithEvent: (UIEvent *) receivedEvent {
if (receivedEvent.type == UIEventTypeRemoteControl) {
NSLog(@"subtype: %d", receivedEvent.subtype);
switch (receivedEvent.subtype) {
case UIEventSubtypeRemoteControlPlay:
[streamPlayer startPlay];
break;
case UIEventSubtypeRemoteControlPause:
[streamPlayer startPlay];
break;
case UIEventSubtypeRemoteControlStop:
[streamPlayer startPlay];
break;
case UIEventSubtypeRemoteControlTogglePlayPause:
[streamPlayer startPlay];
break;
default:
break;
}
}
}
Github: Dove trovate l’esempio
L’esempio completo, citato in quest’articolo è disponibile su github all’indirizzo: https://github.com/valvoline/CPStreamPlayer

8 Responses to “T#109 – Key-Value Observing: Costruiamo un semplice lettore multimediale”
22 Maggio 2012
AndreaBel progetto, e se io avessi un flusso audio proveniente da shoutcast o icecast? Riuscirei ad utilizzarlo in questo progetto?
22 Maggio 2012
valvolinestessa cosa. icecast/shoutcat/whatever.
23 Maggio 2012
AndreaBuongiorno
ho seguito alcuni tuoi tutorial su youtube e sono parecchio interessanti e aiutano a capire meglio.
Ti volevo chiedere un favore mi sono fermato sui codici di questa calcolatrice e volevo chiedere se mi potevi dare una mano
Grazie
http://lnx.fantasylands.net/aiuto-dislessia/wp-content/uploads/2010/08/SHARP-EL-W531G.pdf
Questo è quello che volevo fare.
23 Maggio 2012
Lucabellissimo articolo!!
ma quali sono le differenze tra kvo e il sistema dei delegati?! non conosco l’oggetto AVPlayer ma, in generale, quasi tutti gli oggetti inclusi in ios hanno il supporto per i delegate, per cui si potrebbe utilizzare quella tecnica al posto di questa kvo. sbaglio!?
24 Maggio 2012
andreabernsquindi se nn ho capito male questo NSURL *streamUrl = [NSURL URLWithString:streamAddress];
diventerà [NSURL http:000.000.000.000:0000]; o sbaglio
grazie
3 Giugno 2012
Carlofunziona questo tutoria su xcode 4.3?
6 Giugno 2012
Francesco BurelliQuello che dici è vero, la differenza tra il delegato e il KVO è leggera ma sostanziale, soprattutto se parliamo di classi che implementi tu.
Metti che costruisci una classe che col tempo cambia e vuoi che la vista lo sappia e si aggiorni, col delegato dovresti creare una variabile che punti al delegato e un protocollo che questa deve implementare, mentre col KVO puoi avere N delegati che si aggangiano al cambiamento di una o più proprietà del tuo oggetto GRATIS, senza scrivere una linea di codice!
17 Maggio 2013
roma-traslochiQuesto è il blog giusto per tutti coloro che vogliono capire qualcosa su questo argomento. Trovo quasi difficile discutere con te (cosa che io in realtà vorrei… haha). Avete sicuramente dato nuova vita a un tema di cui si è parlato per anni. Grandi cose, semplicemente fantastico!