Come realizzare un’app per la lettura di riviste – Parte II – Newstand Magazine App

Icona Newsstand Icon

La rivoluzione iOS5

Molta acqua è passata sotto i ponti da quando abbiamo pubblicato la nostra prima parte di questo tutorial. Ci scusiamo per il ritardo, ma al momento di scrivere eravamo consapevoli delle nuove funzionalità introdotte con iOS5 ma eravamo ancora sotto NDA e non autorizzati a rivelare nulla del SDK. Finalmente siamo ora in grado di fornire l’esempio  della nostra applicazione per riviste e magazine  con compatibilità doppia: iOS4/iOS5 .

Non passeremo il tempo a spiegare tutte le caratteristiche della funzione Newstand (edicola), tutto sommato stiamo dandovi le informazioni per creare un app per leggere delle riviste e i dettagli d’implementazione Newstand  vanno oltre il nostro scopo originale.
Abbiamo scritto un tutorial di due parti, presente in inglese nel  blog personale del nostro CTO, che potete leggere qui, e che copre tutti gli aspetti Newstand e le richiesta (da parte di revisori Apple) in caso di abbonamenti.

In breve, Newstand (edicola) è un nuovo modo di presentare riviste nel iPad e iPhone, dove è sostituita l’icona originale dalla copertina della rivista o giornale, ed è posto sotto uno speciale gruppo dedicato a raccogliere tutte le icone Newstand installate dall’utente.
Per lo sviluppatore c’è anche un framework, chiamato Newstand Kit, che introduce una nuova metodologia per organizzare, scaricare e installare il contenuto dell’ App.

L’esempio di applicazione

La schermata mostra l’aspetto finale dell’ app. Un insieme di nove riviste con ongnuna un differente frutto in copertina.

È possibile scaricare una rivista, vedere il cambiamento barra di avanzamento mentre l’operazione è in corso e, infine, leggerla. La schermata mostra l’altro aspetto nel Newstand (edicola).
L’icona tipica è stata sostituita dalla rivista, come la rappresentanza all’interno del gruppo Edicola.
Invece, se eseguito su un iPad con iOS4 installato apparirà l’icona classica.

Il codice di un’applicazione completa è disponibile su GitHub. Non considerarla come un codice di produzione, quindi per favore non cercate di riutilizzare così com’è per la vostra app o con i vostri clienti se non avete provato in tutti i possibili casi limite.
In ogni caso può essere considerato come un valido punto di partenza per le applicazioni reali. In sostanza i principi alla base dell’architettura del codice sono stati descritti nella Parte I di questo tutorial, se non avete ancora letto vi consiglio di saltare al precedente articolo prima di leggere questo, almeno per capire i componenti chiave di tale applicazione.
Lo scopo di queto tutorial , guida o howto, è quello di capire e tenere la gestione dell’ issues management (controller, da non confondere con “View Controller”) separata il più possibile dalle UI (interfaccie utente). Ciò significa che in teoria il codice che sta dietro Store Manager and Issue Models  può essere riutilizzato anche in un applicazione Mac OSX , in quanto rapporto con cose come la UI è minimo.

Abbiamo installato questa applicazione utilizzando il modello di base di una singola finestra Xcode (the basic single window Xcode template) e poi aggiunto le due componenti principali del metodo all’avvio dell’applicazione (delegate startup method):

– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{    
    // here we create the “Store” instance
    _store = [[Store alloc] init];
    [_store startup];     self.shelf = [[[ShelfViewController alloc] initWithNibName:nil bundle:nil] autorelease];
    _shelf.store=_store;     self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
    self.window.rootViewController = _shelf;
    [self.window makeKeyAndVisible];
    return YES;
}

I due componenti sono:

la classe Store, che rappresenta il blocco Store Manager nell’architettura applicazione, questa classe è una sottoclasse di NSObject e non è legata all’interfaccia utente.
Il ShelfViewController rappresenta l’UI (interfaccia utente) dell’applicazione. E’ collegata allo Store (negozio) utilizzando una proprietà specifica. Si noti che il controller non avrà accesso alle proprietà Store esposte, ma otterrà le informazioni necessarie attraverso una semplice API.
Avremmo potuto utilizzare un modello di delega (delegate pattern), invece, ma poichè i due blocchi sono blocchi essenzialmente custom in un’applicazione personalizzata, non c’è proprio bisogno di definire un protocollo per la loro interazione. Possiamo dire che la parte del controller è stato suddiviso in due parti, una per l’interfaccia utente (UI), la mensola (the shelf), e uno per il back-end, il negozio (Store). Il loro legame è tale che la mensola dipende dallo Store (strict link), non viceversa.
Ogni Store per la comunicazione con il prodotto (shelf) utilizza le notifiche.

L’applicazione è stata poi integrata con le nuove esigenze Newstand nel plist info. Anche in questo caso dare un’occhiata alla documentazione di Apple per una guida dettagliata.

 

Modelli e Controller

Abbiamo un modello in questa applicazione, è il modello Issue che rappresenta un numero singolo del magazine o rivista nello Store (negozio) o nella libreria utente (user library). C’è anche un controller, che è il Store. Come abbiam detto questo controller non è un componente di UI (interfaccia utente), ma l’applicazione di back-end è autosufficiente con questi due componenti. In teoria siamo in grado di recuperare lo statuo negozio e riviste scaricate (store status and download magazines) anche senza alcuna UI (interfaccia utente). Questo è un concetto di base in applicazioni per riviste (Magazine Apps), in cui molti eventi avvengono in background non sono legati all’interazione diretta dell’utente : questo significa che l’applicazione dovrebbe essere in grado di eseguire diverse operazioni, anche se l’interfaccia utente non è stata caricata.

Il ruolo della “issue class” è quello di rappresentare tutte le proprietà rilevanti rivista, che sia un identificatore univoco, un titolo, una data di uscita, un collegamento alla immagine di copertina, ovviamente il link (URL) al contenuto (che può essere un file PDF, un file epub o un file zip per i pacchetti complessi).
In particolare, l’identificatore univoco (unique identifier) è un pezzo di dati che deve essere mantenuta lungo la vita dell’ issue: è l’unico modo per identificare univocamente un determinato issue, indipendentemente da problemi di localizzazione (ad esempio, il titolo può cambiare in diversi paesi, ma non l’ID univoco). Inoltre è il link per il modo in Newsstand (edicola) per identificare le issue  (il nome del campo NKIssue) e può anche essere utilizzato per collegare il prodotto con l’App Store, se abbiamo intenzione di implementare gli Acquisti In App o In App Purchases.

Oltre a  questo ruolo, le issue class avranno un ruolo centrale nel download di una rivista. In sostanza la classe Store (Store class) potra fare uno schedule e quindi potra’ programmare un download, ma questo sarà monitorato  dalla issue class (progress) e poi terminare (installazione effettiva della rivista/magazine ).

Infine, questa classe ha limitate capacità di rappresentare una rivista già scaricata e disponibile nella libreria utente (user library). Per questo mettiamo a disposizione la semplice isIssueAvailableForRead  che sara utilizzata da parte dell’interfaccia utente per sapere quali interventi sono possibili con un issue (leggere o scaricare) ed eventualmente visualizzarne il contenuto.

La classe Store è il controller app. E ‘inizializzato all’inizio delle app e la sua istanza non è mai rilasciata. Subito dopo l’inizializzazione DI questa classe verrà recuperato il contenuto dello store (negozio) dal server dell’editore.

In questo esempio abbiamo deciso di attuare il contenuto negozio come un semplice elenco delle proprietà, e poi istruire il negozio per recuperare questa lista di proprietà, decodificarlo e creare gli oggetti di emissione e, infine, scaricare le loro immagini di copertina. Tutto questo è fatto in modo asincrono utilizzando la Grand Central Dispatch e alla fine una notifica verrà inviata per informare tutti gli oggetti interessati (in particolare il controller della vista) che i contenuti Store sono pronti e l’interfaccia utente può essere tranquillamente visualizzata.
Si noti che il negozio è stato rappresentato da un property status. Abbiamo ignorato il  setter method, di inviare una notifica che informa ogni osservatore interessato del nuovo stato. Per semplicità abbiamo deciso di limitare i possibili stati di “non inizializzato”, “download”, “pronto” e “errore”. Una applicazione più compless può introdurre gli stati extra se necessario. Infine come ripiego in caso di collegamento mancante (l’utente deve essere in grado di accedere al suo / suoi contenuti, anche se non collegato a Internet) si potrà ricaricare una copia salvata in locale del negozio.

Il pezzo centrale del codice è nel metodo downloadStoreIssues della classe, che riportiamo di seguito:

-(void)downloadStoreIssues {
self.status=StoreStatusDownloading;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0), ^{
NSArray *_list = [[NSArray alloc] initWithContentsOfURL:[NSURL URLWithString:@”http://www.viggiosoft.com/media/data/iosblog/magazine/store.plist”]];
if(!_list) {
// let’s try to retrieve it locally
_list = [[NSArray alloc] initWithContentsOfURL:[self fileURLOfCachedStoreFile]];
}
if(_list) {
// now creating all issues and storing in the storeIssues array
[_list enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSDictionary *issueDictionary = (NSDictionary *)obj;
Issue *anIssue = [[Issue alloc] init];
anIssue.issueID=[issueDictionary objectForKey:@”ID”];
anIssue.title=[issueDictionary objectForKey:@”Title”];
anIssue.releaseDate=[issueDictionary objectForKey:@”Release date”];
anIssue.coverURL=[issueDictionary objectForKey:@”Cover URL”];
anIssue.downloadURL=[issueDictionary objectForKey:@”Download URL”];
anIssue.free=[(NSNumber *)[issueDictionary objectForKey:@”Free”] boolValue];
[anIssue addInNewsstand];
[storeIssues addObject:anIssue];
[anIssue release];
// dispatch cover loading
if(![anIssue coverImage]) {
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:anIssue.coverURL]];
if(imgData) {
[imgData writeToURL:[anIssue.contentURL URLByAppendingPathComponent:@”cover.png”] atomically:YES];
}
});
}
}];
// let’s save the file locally
[_list writeToURL:[self fileURLOfCachedStoreFile] atomically:YES];
[_list release];
self.status=StoreStatusReady;
} else {
ELog(@”Store download failed.”);
storeIssues = nil;
self.status=StoreStatusError;
}
});
}

 

Si noti che nel codice qui sopra, per semplicità, abbiamo messo in coda il  download della copertina (cover) subito dopo il property list download (download elenco di proprietà).
Probabilmente questo non è il modo migliore per fare questo, stiamo aggiungendo ulteriori networking steps  prima di presentare lo status dell’applicazione, con possibili ritardi se le condizioni della rete non sono buone. Possiamo fare questo in un esempio in cui sappiamo che il numero di problemi è limitato, probabilmente in una applicazione vera e propria ha più senso farlo in un processo in background e quindi notificare all’interfaccia utente di aggiornare il suo status ogni volta che una nuova copertina è pronta da visualizzare.

 

La “View Controller”

L’interfaccia utente si sveglia immediatamente, e cambierà il suo aspetto, basato sulle comunicazioni provenienti dal negozio. Quando il centro di notifica consegnerà al negozio “Sono pronto” come  messaggio per l’interfaccia utente, questo verrà caricato tutte le rappresentazioni problema UI (la classe CoverViewè  nel codice, una view di base con la logica dell’applicazione davvero minimo) e mostrare la shelf (mensola) all’ utente.

A questo punto l’applicazione ha terminato la sua elaborazione back-end ed è in attesa di comandi dell’utente. Qui abbiamo due possibilità:

Una rivista è stata scaricata, l’utente vedrà un pulsante “Leggi”, cliccando su di essa egli sarà in grado di leggerla.
Nel nostro esempio tutti i nostri contenuti sono costituiti di file Pdf, abbiamo deciso di utilizzare il framework Quick Look in iOS, che è sufficiente per i nostri scopi.
Una rivista non è ancora stata scaricata, l’utente vedrà un pulsante “Download”, cliccandoci sopra si avvia il download e ti mostrano una barra di avanzamento (progress bar). Alla fine avremo bisogno di sostituire l’etichetta del pulsante e nascondere il progress. Vedremo questa parte in modo più dettagliato.

La view controller dipende dallo Store (negozio). Al fine di ottenere le informazioni sull’issue(numero dell’issue, il dettaglio di ogni numero/issue) questa non avrà accesso direttamente alle proprietà del negozio, ma userà una API molto semplice, fatta di tre metodi:

/* “numberOfIssues” is used to retrieve the number of issues in the store */
-(NSInteger)numberOfStoreIssues;/* “issueAtIndex:” retrieves the issue at the given index */
-(Issue *)issueAtIndex:(NSInteger)index;/* “issueWithID:” retrieves the issue with the given ID */
-(Issue *)issueWithID:(NSString *)issueID;

Basandoci su questa informazione, e sulle propieta’ dell’ Issue (properties), si creerà la Issue view representation (CoverView) e sarà visualizzata sullo schermo.

Il download di una rivista o giornale

In una Magazine App ci sono tre sezioni critiche:
recuperare e visualizzare i contenuti negozio, scaricare il contenuto e, infine, leggerlo (un buon lettore PDF o EPUB  è obbligatorio in un app del genere, non è lo scopo di questo tutorial, mirato a spiegare l’architettura e le tecniche necessarie per rendere tale applicazione, ma è una parte rilevante l’esperienza utente).

Il nostro approccio nel download della rivista è che l’utente deve essere completamente libero di fare qualsiasi azione e tale azione non deve interferire con il risultato del download.
Ora molte applicazioni sono solite prendere una scorciatoia: hanno messo uno spinner al centro dello schermo, poi bloccano qualsiasi interazione dell’utente con gli oggetti dell’interfaccia utente dietro lo spinner per poi chiedere all’utente di aspettare.
E ‘facile fare questo, ma non è la migliore esperienza utente.
Durante attesa l’utente deve poter leggere un altro numero (issue), poter navigare all’interno del negozio o decidere di passare temporaneamente a un’altra applicazione o infine può perdere temporaneamente il collegamento con la rete.
Newsstand Kit fornisce una metodologia a livello di sistema che semplifica (in alcuni casi) la vita degli sviluppatori, ma al tempo stesso elimina ogni scusa per non fare un buon lavoro in termini di user experience.

Non appena l’utente attiva un nuovo download, il view controller invierà la sua richiesta di download alla classe Store. Nel codice qui sotto potete vedere il scheduleDownloadOfIssue:code.
Questo metodo prepara la richiesta di rete e la invierà in background. Si noti come abbiamo diviso il comportamento applicazione in base alla versione del sistema operativo siamo in. Se è iOS5 allora dobbiamo utilizzare l’approccio Newsstand-Edicola – dove il download saranno gestiti dal sistema in una coda Edicola -, se siamo in iOS4 faremo seguire una metodologia classica basata su NSOperation: in questo caso mi sono perso per la semplicità per ottenere la  content length del contenuto da scaricare, nel caso iOS4 la barra di avanzamento non viene visualizza. Questa informazione è invece fornita “for free” da Newsstand.

-(void)scheduleDownloadOfIssue:(Issue *)issueToDownload {
NSString *downloadURL = [issueToDownload downloadURL];
NSURLRequest *downloadRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:downloadURL]];
if(isOS5()) {
// iOS5 : use Newsstand
NKIssue *nkIssue = [issueToDownload newsstandIssue];
NKAssetDownload *assetDownload = [nkIssue addAssetWithRequest:downloadRequest];
[assetDownload downloadWithDelegate:issueToDownload];
} else {
// iOS4 : use NSOperation
NSURLConnection *conn = [NSURLConnection connectionWithRequest:downloadRequest delegate:issueToDownload];
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(startDownload:) object:conn];
if(!downloadQueue) {
downloadQueue = [[NSOperationQueue alloc] init];
downloadQueue.maxConcurrentOperationCount=1;
}
[downloadQueue addOperation:op];
[downloadQueue setSuspended:NO];
}
}// iOS4 only
-(void)startDownload:(id)obj {
NSURLConnection *conn = (NSURLConnection *)obj;
[conn start];
}

Nei due casi mi piacerebbe sottolineare il fatto che lo “Store” avvia l’operazione di download, ma poi  delega ogni ulteriore elaborazione alla parte interessata, l’ Issue da scaricare. Così sarà la classe Issue che monitorerà il download e poi lo finalizzerà.

La issue class (classe di Emissione) si comporterà come delegato dell’operazione di scaricamento innescata dalla Store class. Il delegate protocolrisulta differente se si sta utilizzando “Edicola” oppure no. Nel primo caso sarà necessario per essere compatibile con il protocollo NSURLConnectionDownloadDelegate mentre nel secondo caso il protocollo da rispettare sara’ il  non documentato (o: documentato solo nel file header ) NSURLConnectionDataDelegate che è una “spin-off” di il NSURLConnectionDelegate classico.
La differenza tra i due protocolli è che il primo scriverà nel file system, l’altro in memoria.
Anche in questo caso non usare il “data” approach a fini di produzione, qui si sta caricando tutto il contenuto in memoria e quindi salvando su disco quando il contenuto è stato completamente scaricato: in realtà non è l’approccio migliore se il contenuto è centinaia di megabyte, la vostra applicazione sarà certamente crash.

Al fine di monitorare visivamente l’avanzamento e quindi aggiornare l’interfaccia utente, abbiamo deciso, essendo coerente dei nostri principi, per tenere l’ application store controller  indipendente per ogni scelta dell’interfaccia utente. Per fare questo ci avvaliamo di KVO e notification. In sostanza quando il view controllerinizia un download, sarà impostato e la “magazine view” come “notification observers” del risultato download, al fine di aggiornare il suo status non appena termina il download (con successo o con errore):

-(void)downloadIssue:(Issue *)issue updateCover:(CoverView *)cover {
cover.progress.alpha=1.0;
cover.button.alpha=0.0;
[issue addObserver:cover forKeyPath:@”downloadProgress” options:NSKeyValueObservingOptionNew context:NULL];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(issueDidEndDownload:) name:ISSUE_END_OF_DOWNLOAD_NOTIFICATION object:issue];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(issueDidFailDownload:) name:ISSUE_FAILED_DOWNLOAD_NOTIFICATION object:issue];
[[NSNotificationCenter defaultCenter] addObserver:cover selector:@selector(issueDidEndDownload:) name:ISSUE_END_OF_DOWNLOAD_NOTIFICATION object:issue];
[[NSNotificationCenter defaultCenter] addObserver:cover selector:@selector(issueDidFailDownload:) name:ISSUE_FAILED_DOWNLOAD_NOTIFICATION object:issue];
[_store scheduleDownloadOfIssue:issue];
}
Sia con la “cover view” che il “view controller”, sarà necessario annullare la registrazione dal centro della notifica non appena termina l’operazione di download. Oltre a permettere la visione di copertura per tenere traccia del progress status, come il download comincia, sarà registrato come “KVO observer” della proprietà DownloadProgress del “issue”. Questo significa che per ogni cambiamento di questa proprietà, che si verifica durante la fase di scaricamento, la cover view sara’ informata del cambiamento per aggiornare la barra di avanzamento (quindi abbiamo qui una separazione di  back-end property, del download progress, dell’ UI, e la UIProgressBar). La cover vieweliminarà la registrazione non appena termina il download.Quando termina il download, l’ Issue instance  (istanza di emissione) copierà il contenuto scaricato fino alla destinazione finale. Nell’ambito Newstandquesta destinazione è specificato dal sistema, in iOS4 lo copia nella directory di cache per essere compatibile con i requisiti icloud (sì, iOS4 non funziona con icloud ma la nostra applicazione è la stessa per entrambe le generazioni del sistema operativo, quindi dobbiamo essere buoni cittadini, in ogni caso).Nel caso Newstand avremo anche bisogno di aggiornare l’immagine di copertina per l’icona Edicola. Lo faremo con un semplice comando all’interno del codice che gestisce la cessazione download (alla fine dobbiamo anche inviare la notifica di fine download di cui abbiamo parlato in precedenza):
-(void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *)destinationURL {
// copy the file to the destination directory
NSURL *finalURL = [[self contentURL] URLByAppendingPathComponent:@”magazine.pdf”];
ELog(@”Copying item from %@ to %@”,destinationURL,finalURL);
[[NSFileManager defaultManager] copyItemAtURL:destinationURL toURL:finalURL error:NULL];
[[NSFileManager defaultManager] removeItemAtURL:destinationURL error:NULL];
// update Newsstand icon
[[UIApplication sharedApplication] setNewsstandIconImage:[self coverImage]];
// post notification
[self sendEndOfDownloadNotification];
}

Conclusioni

Questa è la conclusione di questo tutorial. Abbiamo deciso dopo un lungo intervallo di fare uno sforzo per proporre una applicazione completa valida sia per ambienti iOS4 e iOS5 . Nel codice si trovano cose più interessanti, ad esempio un “aggancio” allo Store Kit: questo è un problema tipico non considerato dagli sviluppatori quando fanno le applicazioni che vendono magazines.
In tal caso la memorizzazione del prezzo del’issue nel server dell’editore non è di aiuto poicheì dobbiamo recuperare le informazioni sui prezzi alla fonte, che è l’App Store. Per fare questo la nostra applicazione deve – in modo asincrono – fsre query su iTunes Store per ottenere i prezzi di tutte le pubblicazioni e quindi visualizzarle una volta recuperate.

Non abbiamo aggiunto questo ulteriore passaggio nel codice, solo il “gancio” per future estensioni, ma ovviamente, se richiesto dai nostri lettori la avremo possibilità di estendere ulteriormente questo tutorial.

Have a nice Code with

Carlo Vigiani

3 commenti:

  1. Wow, that’s what I was looking for, what a information! existing here at this blog, thanks admin of this website.

  2. New World Hacker

    Thanks a lot, is a very cool article.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

*