Con l’introduzione dell’iPad gli sviluppatori si son trovati di fronte al dilemma se sviluppare applicazioni separate per iPhone e iPad oppure rilasciare un’unica app “universale” valida per entrambi i dispositivi.

Indipendentemente da eventuali scelte di marketing, che potrebbero far propendere verso applicazioni separata col vantaggio di poterne vendere due al posto di una, in questo articolo spiegheremo quali sono i vantaggi che si possono ottenere dallo sviluppo di una singola applicazione e alcuni utili suggerimenti su come impostare il lavoro.

Prima di addentrarci nelle caratteristiche di un’applicazione universale, e’ bene rammentare come avviene il caricamento e il lancio di un’applicazione in iOS.

CARICAMENTO E LANCIO DELL’APPLICAZIONE

La figura in basso, tratta come sempre dall’eccellente documentazione Apple fornita col gratuitamente col pacchetto XCode dal sito http://developer.apple.com, mostra qual e’ la sequenza degli eventi che si scatena dal momento in cui l’utente tocca sull’icona dell’applicazione che intende lanciare.

app_life_cycle.jpg

Essendo Objective-C nient’altro che un’estensione del C, un’applicazione parte con l’esecuzione della funzione main(). Utilizzando i template forniti con XCode, la funzione main() contiene il seguente codice, che e’ bene non modificare mai a meno di esigenze molto particolari:

Quel che fa questo codice e’ essenzialmente lanciare la funzione UIApplicationMain() il cui compito consiste innanzitutto nell’instanziare l’oggetto UIApplication, il cui scopo e’ quello di creare e eseguire il motore delle applicazioni iOS, il cosiddetto Event Loop. Non entreremo in quest’articolo nella descrizione dell’Event Loop (lo faremo prossimamente) ma per ora basti sapere che ogni qualvolta l’utente tocca lo schermo del suo dispositivo, il tocco viene catturato dall’Event Loop e smistato alla porzione di codice che si occupera’ di gestirlo.

Dato che l’oggetto UIApplication e’ unico e gestito dal sistema operativo, come facciamo a inserire il nostro codice all’interno di questo meccanismo di start-up dell’applicazione? La risposta e’: tramite l'”application delegate”. Uno dei compiti della funzione UIApplicationMain e’ infatti quello di instanziare il nostro Application Delegate e, durante la fase di lancio, fornirgli il controllo tramite la ben nota chiamata al metodo “application:didFinishLaunchingWithOptions:”. In questo metodo noi inseriremo la nostra logica iniziale, creeremo i nostri view controller e comunque ci consentira’ di far partire la nostra app.

Riassumendo: l’utente tocca l’icona dell’applicazione, il sistema operativo carica l’app e chiama la funzione “main()”, la funzione main chiama la funzione UIApplicationMain, quest’ultima instanzia l’oggetto (unico) UIApplication, il quale avvia l’Event Loop e chiama il suo “delegate” affinche’ quest’ultimo esegua il codice di inizializzazione proprio dell’applicazione.

Quel che resta da definire a questo punto e’: come fa UIApplication a conoscere il suo “delegate”? la risposta e’: questa informazione viene fornita tramite il file Info.plist con la chiave “NSMainNibFile” (indicata in XCode come “Main nib file base name”). Questo nib file, che in genere nei template di XCode si chiama MainWindow.xib, conterra’ un oggetto AppDelegate che sara’ collegato all’outlet “delegate” del File’s Owner, che non e’ nient’altro che il nostro oggetto UIApplication.

Quindi UIApplicationMain svolgera’ oltre al compito sostanziale di instanziare UIApplication anche quello di caricare l’NSMainNibFile e collegare l’instanza di UIApplication (nota come “sharedApplication”) all’AppDelegate.

Questa lunga spiegazione e’ necessaria per capire come far partire un’app universale. In genere i template di XCode per le applicazioni univesali forniscono due diversi NSMainNibFile, uno denominato NSMainNibFile – e usato di default – un altro denominato NSMainNibFile~ipad che, se definito, verra’ caricato nel caso l’app venisse lanciata da un’iPad.

In genere le due chiavi devono puntare a due finestre differenti, dato che sono differenti le dimensioni degli schermi, e il programmatore potrebbe scegliere di instanziare i due UIViewController iniziali, che potrebbero essere profondamente diversi, gia’ nel nib file iniziale. Quel che pero’ fa il template di XCode e’ collegare all’UIApplication delegate outlet due diverse istanze di AppDelegate, denominate rispettivamente AppDelegate_iPhone e AppDelegate_iPad.

Da questo momento in poi le due app prenderanno strade sostanzialmente differenti e sara’ compito del programmatore gestire i due “rami” di codice e cercare di accomunare tra loro quelle porzioni di codice che sono indipendenti dall’interfaccia grafica.

Compito di questo articolo e’ indicare alcune regole pratiche che faciliteranno il compito del programmatore cercando di accomunare il piu’ possibile le porzioni di codice.

APPLICATION DELEGATE UNIFICATO

Il primo approccio rivolto all’unificazione del codice e’ quello di definire un unico application delegate. Come detto in precedenza il template “Universal app” di XCode tende a creare due app delegate praticamente identici, uno per iPad e l’altro per iPhone, e referenziarli in due MainWindow.xib differenti, che e’ bene rimangano tali dato che in questa sede si potrebbero gia’ cominciare a definire le peculiarita’ grafice delle due app (se non altro, le due Window hanno gia’ dimensioni differenti!).

Pertanto il nostro approccio sara’ cancellare uno dei due delegate dal nostro codice (ad esempio AppDelegate_iPad) e sostituire nel MainWindow ad esso associato (nell’esempio: MainWindow_iPad) l’oggetto corrispondente.
In questa maniera l’eventuale caricamento di dati essenziali all’applicazione e il codice richiesto dal protocollo di UIApplicationDelegate sara’ comune nei due casi.

ORGANIZZAZIONE DEL PROGETTO

Per mantenere un certo ordine nell’organizzazione del progetto, e’ buona norma definire tre gruppi all’interno di XCode, denominati Shared, iPhone ┬áe iPad, rispettivamente utilizzati per i file comuni, specifici all’iPhone e specifici all’iPad. Nel nostro caso inseriremo l’AppDelegate comune nel gruppo Shared.

FORCHETTE NEL CODICE

E’ ovvio che tanto piu’ si condivideranno delle porzioni di codice, tanto piu’ sara’ necessario discriminare all’interno di tale codice se si sta eseguendo l’app in un iPad o in un iPhone/iPod Touch, dato che sarebbe presuntuoso pensare di poter scrivere del codice sempre indipendente dal device, specialmente nei View Controller e nelle View.

A tal fine e’ bene aggiungere nel proprio file “Prefix.pch” la definizione seguente:

La funzione isPad(), una volta chiamata, restituira’ YES se l’app viene lanciata in un iPad, NO altrimenti. Questo codice si basa sulla funzione “userInterfaceIdiom” definita nella classe UIDevice. Se questo metodo fornisce in risposta la costante UIUserInterfaceIdiomPad allora siamo in un iPad, altrimenti no. Pero’, dato che questa funzione e’ stata introdotta solamente con la versione 3.2 di iOS, ossia quella che ha introdotto l’iPad (in questo possiamo dire che in Apple non sono stati molto lungimiranti!), per poter garantire la compatibilita’ con versioni precedenti di iOS dobbiamo controllare che questa funzione esista e possa essere chiamata (se non lo facessimo, la nostra app andrebbe miseramente in crash). E’ ovvio che una risposta negativa a questo check implicherebbe una versione di iOS precedente alla 3.2 e quindi il dispositivo non puo’ essere un iPad.

Quindi, tutte le volte che dovremo gestire una qualche eccezione tra i due dispositivi, ci sara’ sufficiente creare una condizione if…else… su isPad() e gestire il comportamento dell’app nei due casi.

IL PATTERN MVC

La programmazione in iOS si basa profondamente sul paradigma Model-View-Controller (MVC).
Questo pattern architetturale prevede che un software sia articolato nelle tre unita’ fondamentali: Modello, Vista e Controllore. Il modello si occupa della gestione dei dati di un’applicazione, le viste gestiscono l’interfaccia utente e la rappresentazione grafica mentre il controller si occupa della logica dell’applicazione mettendo in comunicazione il modello con la vista.

E’ evidente che il Modello prescindera’ sempre dalla natura del dispositivo, in particolare nel nostro caso saremo facilitati dal fatto che sia l’iPhone che l’iPad condividono esattamente le stesse classi e le stesse funzioni per tutto cio’ che riguarda la gestione dei dati (le cosiddette “foundation classes”, ma anche il file system e i vari media framework o l’accesso ai sensori). Pertanto massima cura dovra’ essere data al progetto dell’app affinche’ i moduli comuni non direttamente legati alla rappresentazione grafica siano spostati all’interno del modello.

Talvolta si tende a integrare alcune funzionalita’ all’interno dei View Controller, questo per semplicita’ nella scrittura del codice: dato che il View Controller gestiste il flusso dell’applicazione perche’ non inserire certe funzionalita’, quali ad esempio quelle di networking, all’interno del View Controller? sebbene questo approccio continui ad essere sempre valido, occorre considerare il fatto che in taluni casi non e’ conveniente condividere tra i due dispositivi lo stesso View Controller, e in tal caso tali funzionalita’ andrebbero replicate con evidenti problemi di manutenzione del codice. Per cui si consiglia di spostare quanto piu’ possibile queste operazioni all’interno dei modelli, e sfruttare i meccanismi di comunicazione di Objective-C (protocolli, KVO, notifiche) per far colloquiare i modelli coi controller.

VISTE CONDIVISE

Condividere le viste, ossia le varie UIView e tutte le loro sottoclassi, e’ compito in genere abbastanza arduo. In genere si sconsiglia di costruire una vista per iPad partendo da quella per iPhone e “ingigantendola” di conseguenza: l’effetto e’ abbastanza sgradevole e comunque non renderebbe merito del magnifico display dell’iPad.

Quindi il consiglio che ci sentiamo di dare e’ quello di cercare di studiare l’interfaccia grafica tenendo conto delle caratteristiche dei due dispositivi e solamente in seguito pensare se e come unificare parti di essa. Vi saranno sicuramente dei casi in cui le viste possono essere condivise: si pensi ad esempio alle piccole miniature che si vedono in fondo al lettore di PDF integrato nell’app iBooks. Esse possono essere costruite (diciamo “possono” poiche’ non sappiamo se i progettisti di Apple abbiano seguito lo stesso approccio) usando un’unica vista che calcolando le dimensioni dello schermo sara’ in grado di conoscere il numero di miniature che potranno essere posizionate. Inoltre le stesse dimensioni delle miniature, dato che saranno diverse fra i due schermi, potranno essere discriminate grazie all’uso della nostra funzione isPad(). Quindi senza ombra di dubbio un’eventuale classe “ThumbnailsView” potra’ essere inserita nel gruppo Shared del nostro progetto.

VIEW CONTROLLER

Non c’e’ ombra di dubbio che i View Controller, rappresentando la logica dell’applicazione e quindi la parte piu’ complessa, richiedono un’attenta valutazione prima di effettuare la scelta della condivisione o no. Infatti tenere separati i View Controller tra i due rami del progetto (iPhone e iPad) puo’ implicare una quantita’ eccessiva di codice ridondante. Ma allo stesso tempo un’eccessiva unificazione, soprattutto per quei View Controller che piloteranno viste molto differenti per i due schermi, potrebbe comportare a una tale ramificazione intra-codice e un numero cosi’ grande di biforcazioni da rendere il codice difficilmente leggibile.

Si tenga conto inoltre che certi elementi molto comuni nell’iPhone, come gli UITabController, sono raramente utilizzati nell’iPad, e quindi questo gia’ in partenza implica una netta separazione delle due porzioni di codice. Chiaramente gli UITabController istanziano a loro volta dei UIViewController che potrebbero essere unificati tra loro. Si pensi per esempio al caso di una mappa: i percorsi per arrivare alla mappa possono essere molto diversi fra due dispositivi, ma alla fine la vista, a parte alcune differenze, sara’ sostanzialmente la stessa: una mappa a tutto schermo con una toolbar o navigation bar per accedere ad altre funzioni.

Il prossimo paragrafo, sui Popover, fornira’ una soluzione molto comoda per unificare il maggior numero di View Controller possibile.

I POPOVER

L’iPad e’ dotato di un insieme di view controller unici, quali gli UISplitViewController e gli UIPopoverController.

Questi ultimi sono quei “fumetti” che appaiono di tanto in tanto nell’interfaccia delle app per iPad e che consentono di accedere ad alcune funzionalita’ extra dell’applicazione. Apple suggerisce nelle sue Human Interface Guidelines (HIG) di utilizzare questo controller tutte le volte che si vuole accedere ad alcuni sezioni dell’app senza distogliere l’utente dalla pagina principale. Questo approccio e’ molto diverso dall’iPhone, in cui il progresso all’interno di un’applicazione avviene tipicamente tramite le “sliding windows”, ossia le finestre scorrevoli da destra verso sinistra e gestite da un UINavigationController. A ragione, i redattori dell’HIG ritengono che far scorrere tali finestre in un’app per iPad non sarebbe il massimo, poiche’ date le loro dimensioni il tutto ┬árisulterebbe essere oneroso per il processore (rendering grafico) e visivamente pesante. In questo la soluzione suggerita e’: aprire un popover all’interno della finestra e inserire il view controller al suo interno. E’ vero che nel tempo gli sviluppatori si sono sbizzarriti nell’ottenere soluzioni alternative a questo approccio, ad esempio con viste scorrevoli parzialmente, ma l’approccio dei popover risulta essere il piu’ immediato, abbastanza elegante e comunque perfettamente in linea con le HIG.

I popover possono essere sfruttati per unificare i view controller. Si consideri ad esempio il caso di una funzione “Impostazioni” che si vuole introdurre nell’app. Ovviamente date le differenti dimensioni dello schermo, per l’iPhone si potrebbe impostare una classica vista 320×480 contenente una tabella e mostrata per scorrimento. Nell’iPad occorrerebbe costruire una macro vista 768×1024 da mostrare in qualche maniera e magari difficile da riempire (e quindi da rendere esteticamente gradevole) soprattutto se il numero di impostazioni risultasse ridotto. In questo caso la soluzione migliore sarebbe creare un popover e inserire al suo interno una finestra di dimensioni, guarda caso, 320×480.

Pertanto avremmo lo stesso ImpostazioniViewController, studiato per un iPhone, e le differenze fra i due device sarebbero:

  1. chiamata via NavigationController (“push”) per iPhone, via PopoverController per iPad
  2. presenza della navigation bar con “Back” button per iPhone, presenza opzionale della navigation bar ma senza “Back” button

LE MODAL VIEW

Le modal view sono tradizionalmente utilizzate nell’iPhone per interrompere il flusso dell’applicazione e porre l’utente di fronte ad un’attivita’ che richiede la sua immediata e unica attenzione. Con l’iPad tali viste sono state estese per consentire un approccio diverso all’interazione con l’utente, ed e’ grazie a tale scelta che possiamo pensare ad un’ulteriore forma di condivisione del codice.

La differenza sostanziale e’ che mentre nell’iPhone la modal view appare sempre e soltanto dal basso verso l’alto a riempire tutto lo schermo, nell’iPad e’ possibile scegliere tra diversi approcci. Nel nostro caso l’approccio piu’ confacente alla condivisione e’ quello di mostrare la vista modale in modalita’ “Form Sheet”, con la quale la modal view appare al centro dello schermo, con dimensioni ridotte, con un effetto di “dimming” nelle aree lasciate scoperte.

In questo caso la nostra vista comune non avra’ le stesse dimensioni (nell’iPad e’ comunque piu’ grande) per cui dovremo gestirla affinche’ il ridimensionamento risulti adeguato. Ad ogni modo queste eccezioni possono essere facilmente gestite nel nostro codice tramite l’uso della funzione isPad() senza dover necessariamente ricorrere all’uso di View Controller separati.

CONCLUSIONI

Senza dubbio scrivere una buona app universale rapprsenta sempre una sfida. Ma se non altro i vantaggi che si ottengono in riutilizzabilita’ e manutenzione valgono lo sforzo. Inoltre l’impegno in piu’ richiesto per la definizione di un’architettura che favorisca la condivisione del codice verra’ premiato da un software che risultera’ essere piu’ ordinato, leggibile e estendibile.

Chiaramente questo obiettivo puo’ non essere facilmente raggiungibile, e invitiamo gli sviluppatori a non cimentarsi in imprese disperate per rendere universali tutte le app, soprattutto in quei casi in cui le due interfacce sono profondamente diverse, oppure il contributo dovuto ai modelli e’ insignificante oppure si proviene da un’app iPhone gia’ esistente e progettata con tutt’altri criteri.