Indubbiamente, rispetto ad altri grandi cambiamenti come un linguaggio di programmazione completamente nuovo, alcune novità del nuovo SDK introdotte nell’ultima WWDC (2014) hanno avuto minore risonanza.
Una di queste è sicuramente l’entrata in scena nella piattaforma iOS delle Size Classes: di cosa si tratta?
Adaptive
La parola che meglio descrive l’idea centrale delle migliorie portate all’SDK da iOS8 è Adaptive. Il termine indica la possibilità di scrivere codice che, appunto, si “adatta” a diverse situazioni e che, di conseguenza, è riusabile (tutti sappiamo quanto questo sia importante per noi sviluppatori).
In pratica la filosofia adaptive si concretizza nei seguenti 5 punti cardine del nuovo SDK:
- Size Classes
- Adaptive View Controller
- Adaptive Presentations
- Adaptive Text e Tables
- Extensions
In questo post cerco di spiegare il primo punto: Size Classes. Gli altri punti sono ugualmente importanti, ne riparleremo.
Device e orientation
Ad oggi le indicazioni dei nostri designers riguardanti l’interfaccia della app dipendono e si modificano a partire da due elementi fondamentali:
- la device su cui gira la app (iPad, iPhone)
- l’orientamento della device stessa (portrait, landscape)
Abbiamo quindi varie combinazioni di queste casistiche e, in futuro, il numero di combinazioni è destinato ad aumentare, infatti già si rumoreggia di nuove device in arrivo da Apple con dimensioni di schermo ancora diverse da quelle esistenti.
In realtà però, pensandoci bene, le modifiche al layout di pagina dipendono solo da due grandezze: la dimensione orizzontale e la dimensione verticale dello schermo.
Fino ad oggi abbiamo scritto molto codice chiedendoci “il corrente orientamento della device”, ma quello che volevamo sapere veramente era “la dimensione della finestra di disegno (canvas)”.
E addirittura per le device abbiamo, sempre fino ad oggi, dovuto mantenere due file diversi di storyboard, uno per l’iPhone e uno per l’iPad. Probabilmente la Apple non se l’è sentita di farci mantenere un terzo storyboard per un’altra device che uscirà quest’autunno, dunque ha affrontato il problema da un punto di vista diverso.
Compact e Regular
In XCode 6 possiamo disegnare la nostra interfaccia ignorando l’informazione “su quale device siamo” o “in quale orientamento siamo” ma basandoci solo sulle dimensioni correnti, o meglio, su certe “fasce” dimensionali. Immaginando di dividere le possibili dimensioni in classi, progettiamo un’interfaccia contenente in se le informazioni utili per adattarsi alle varie classi dimensionali in cui si presenterà.
Le possibili classi dimensionali (size classes) sono:
- Compact
- Regular
La dimensione Regular è maggiore di quella Compact. Differenti device e orientamenti corrispondono a dimensioni compact o regular sui due assi, in particolare il seguente schema riassume la situazione per le device esistenti in questo momento:
Per esempio un iPhone in orientamento portrait ha la dimensione verticale Regular e orizzontale Compact. L’iPad ha dimensioni Regular sia in portrait che in landscape (attualmente non esiste device con larghezza regular e altezza compact).
Abbiamo spostato il problema chiedendoci se stiamo consumando una coca cola piccola (compact) o grande (regular) e la nostra coca cola sono le dimensioni orizzontale e verticale di iPhone, iPad o … altro.
Interface builder
La maniera migliore per capire queste novità è usare le size classes in XCode (>= 6). Poi vedremo come avvalerci delle size classes anche programmaticamente.
Creiamo un progetto in XCode (Simple View Application – Universal – Swift). Apriamo il Main.storyboard e vediamo che il canvas è quadrato! Non ci siamo abituati, non esiste una device con questa forma. Infatti XCode ci vuole proprio dire che non stiamo progettando per una device in particolare, ma ci stiamo basando sulle size classes.
Notiamo che, avendo creato un progetto nuovo, XCode ci ha automaticamente “iscritti” all’uso delle Size Classes:
Se vogliamo possiamo deselezionare l’uso delle Size Classes in XCode e continuare a lavorare alla vecchia maniera (da sapere: le Size Classes non possono funzionare se non è abilitato anche Auto Layout).
Alcune note sulla compatibilità prima di proseguire: se usiamo solo le funzionalità di Interface Builder la nostra app continua ad essere retro compatibile senza problemi (fino a iOS6), non vengono inserite incompatibilità (ma cambia il formato del file di XCode).
Se invece usiamo da codice le nuove classi introdotte (come UITraitCollection di cui parlerò fra poco) la nostra app sarà compatibile solo con iOS8 (e scriveremo codice condizionale per renderla retro compatibile).
Come al solito il consiglio è di lavorare per quanto possibile in Interface Builder.
Tornando al nostro progetto, lo storyboard presenta ora, in basso, un bottone w Any h Any.
Premendo il bottone ci viene presentato un popover che indica le size classes per cui valgono le indicazioni di layout che stiamo dando.
Per ogni dimensione abbiamo 3 possibilità di design:
- solo compact
- solo regular
- entrambi (any)
Quindi le possibili combinazioni, considerando altezza e larghezza, sono 9. Questa matrice 3×3 è riportata nel popup mostrato al click del bottone. Nel caso della figura stiamo decidendo di progettare una interfaccia per Any – Any, cioè senza nessuna distinzione particolare (la matrice riporta gli elementi any su riga (altezza) e colonna (larghezza) centrali).
Possiamo modificare l’indicazione spostando (click and drag) il rettangolo azzurro in diverse configurazioni, per esempio ridimensionando il rettangolo nella prima colonna diciamo a XCode che le indicazioni che stiamo dando sono solo per wCompact hRegular (che al momento corrispondono ad un iPhone in portrait):
Notiamo anche che la fascia in basso è diventata blu, indicazione del fatto che stiamo dando regole per un caso particolare (cioè non siamo in wAny hAny).
In genere lavoriamo sulla interfaccia nel seguente modo: partiamo da wAny hAny e disegniamo l’interfaccia. Poi andiamo a modificare casistiche particolari per casi compact/regular. In questo modo siamo sicuri di gestire tutti i casi possibili e indicare le “eccezioni” per i casi particolari.
Un po’ di pratica
Vediamo nella pratica come usare tutto questo. Nel Main.storyboard inseriamo una ImageView con 2 UILabel (titolo e descrizione) affiancate e 1 UIButton “Dettagli” più in basso con i seguenti constraint:
- la Image View con un bordo di 20pt dalla main view e in mode: aspect fill
- il titolo a 40pt-40pt dal top left della main view
- la descrizione a 20pt in orizzontale dal titolo e larghezza 192pt, e allineata top con il titolo
- il bottone “Dettagli” a 40pt dal margine sinistro e allineato (bottom) con la descrizione
Il risultato dovrebbe essere il seguente (trovate tutti i dettagli nel codice di esempio alla fine dell’articolo):
Da XCode possiamo già vedere una anteprima di questo design nelle varie device/orientation. Apriamo l’assistant editor e, dal menu relativo, scegliamo “Preview”:
Un simbolo ‘+’ nella finestra di preview ci consente di aggiungere un tipo di device, e un simbolo “ruota” (che compare solo on mouse over) sotto ogni device ci permette di esercitare la rotazione per studiare il comportamento dell’interfaccia in vari orientamenti.
(si noti la scritta “English” in basso a dx, queste preview possono essere utilizzate anche per testare l’adattamento del layout alle diverse lingue dell’app!)
Vediamo il risultato per un iPhone in verticale:
Notiamo subito che il nostro design va bene per una larghezza “regular” (sufficientemente grande) ma non per la compact, se infatti ruotiamo il canvas vediamo un layout corretto:
A questo punto quindi eseguiamo delle modifiche al layout, ma solo nel caso di larghezza compact, per far questo selezioniamo la configurazione dal menu “Class Sizes”:
La barra inferiore diventa blu per indicare che stiamo lavorando in un caso particolare (cioè diverso da any – any).
In questa modalità vorremo allineare il testo di descrizione sulla sinistra. Analizziamo il constraint che definisce la posizione orizzontale della descrizione (20 pt di distanza orizzontale dal titolo):
Premiamo il (secondo) tasto “+” per aggiungere una “Size Class Customization”:
Scegliamo la configurazione attuale (Compact Width | Regular Height (current)) e poi deselezioniamo il box “Installed” per wC hR:
Stiamo dicendo al sistema di ignorare questo constraint per la configurazione wC hR.
(Lo stesso risultato potevamo ottenerlo eliminando semplicemente il constraint; infatti questo sarebbe stato cancellato solo dalla configurazione di lavoro corrente (wC hR), vediamo questo modo di procedere nel passo successivo.)
Aggiungiamo ora un constraint che allinea a sinistra la descrizione col testo: questa volta il constraint verrà automaticamente installato solo nella configurazione corrente (wC hR), dato che stiamo lavorando in una configurazione ben precisa (diversa da wAny hAny).
Infine dobbiamo spostare il bottone “Dettagli” più in basso. A scopo dimostrativo lo facciamo lasciando il constraint che allinea i bottom di testo e descrizione e modificando il suo valore (la sua costante). Premiamo stavolta il primo tasto “+” (quello accanto a constant) e inseriamo (per la attuale configurazione: wC hR) il valore 40.
Il risultato finale è il seguente per una larghezza compact:
Dunque abbiamo appena realizzato un layout che si comporta diversamente a seconda delle classi di grandezza in cui operiamo. E con un solo storyboard per più device!
Operazioni per-class-size
I due tipi di operazioni al momento eseguibili per-class-size sono:
- Aggiungere, editare, rimuovere constraints di auto layout
- Rimuovere o aggiungere view (ma non modificare caratteristiche di una view)
Forse in futuro si potranno fare altre cose, come per esempio avere una UIButton che ha un title in una size class e un altro title in un’altra size class, al momento questo non è possibile, bisogna avere due UIButton diversi e rimuovere uno o l’altro nei due casi.
In realtà è possibile al momento anche un’altra modifica, e cioè l’uso di font diversi in size class diverse.
Provate infatti a sperimentare con il bottone “+” accanto al menu Font nell’inspector della UILabel (credo che nel tempo vedremo aumentare questi bottoni “+” accanto ad altri attributi):
UITraitCollection
In iOS8 è stata introdotta una nuova classe che descrive i traits di un certo oggetto di interfaccia (UIWindow, UIViewController, UIView e, in generale, tutti gli oggetti conformi al protocollo UITraitEnvironment). I traits descrivono le classi dimensionali di cui abbiamo parlato finora. In particolare una UITraitCollection ha i seguenti campi:
- horizontalSizeClass
- verticalSizeClass
- displayScale
- userInterfaceIdiom
Possiamo vedere il nostro ViewController in che modalità sta operando analizzando la sua proprietà traitCollection.
Nel metodo viewDidLoad del nostro view controller stampiamo la trait collection in cui stiamo operando:
1 2 3 4 5 6 |
override func viewDidLoad() { super.viewDidLoad() if version.doubleValue >= 8 { printTraitCollection(self.traitCollection); } } |
Notiamo che è necessario controllare la versione di iOS prima di usare la traitCollection. version è una proprietà del view controller così definita (usiamo NSString invece di String per la conversione a double, al momento non ancora chiara nelle stringhe swift):
1 |
let version:NSString = UIDevice.currentDevice().systemVersion as NSString; |
(Nella mia attuale versione di XCode è stato necessario aggiungere manualmente UIKit nella sezione “link binary with libraries” in “Build phases” per indicarlo come optional (weak) altrimenti la app in iOS7 non funziona, cioè sembra che Swift non esegua il link weak di default, ma credo che questa sia una caratteristica momentanea dovuta alla rapida evoluzione di Swift e a possibili incompatibilità binario/frameworks; si veda “Binary Compatibility and Frameworks” in questa pagina).
Il metodo printTraitCollection stampa le proprietà della trait collection:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
func printTraitCollection(traitCollection : UITraitCollection) { switch(traitCollection.horizontalSizeClass) { case .Unspecified: println("horizontalSizeClass Unspecified"); case .Compact: println("horizontalSizeClass Compact"); case .Regular: println("horizontalSizeClass Regular"); } switch(traitCollection.verticalSizeClass) { case .Unspecified: println("verticalSizeClass Unspecified"); case .Compact: println("verticalSizeClass Compact"); case .Regular: println("verticalSizeClass Regular"); } println(traitCollection.displayScale); switch(traitCollection.userInterfaceIdiom) { case .Unspecified: println("userInterfaceIdiom Unspecified"); case .Pad: println("userInterfaceIdiom Pad"); case .Phone: println("userInterfaceIdiom Phone"); } } |
Possiamo anche essere notificati quando i traits cambiano implementando nel view controller il metodo:
1 2 3 4 5 6 7 8 9 |
override func traitCollectionDidChange(previousTraitCollection: UITraitCollection!) { println("trait collection from ... "); if let t = previousTraitCollection { printTraitCollection(t); } println("to ... "); printTraitCollection(self.traitCollection); } |
Che per esempio viene invocato quando ruotiamo l’iPhone. Avremmo potuto per esempio modificare i constraint programmaticamente al cambio di trait collection, ma è comunque meglio usare l’interfaccia grafica (Interface Builder) anche per problemi di compatibilità con iOS7.
Simulatore resizable
La nuova versione del simulatore disponibile con XCode 6 ci permette di testare la nostra app anche in device (virtuali) al momento non esistenti permettendoci di modificare le dimensioni del simulatore:
Nel simulatore possiamo poi definire nella barra in basso le dimensioni della finestra (e le classi associate). Per esempio possiamo lanciare la nostra app in una ipotetica device di dimensioni 500×500 (wR hR):
UIImageAsset
Come ultima nota indichiamo che è anche possibile usare, nella nostra interfaccia, immagini diverse a seconda della size class corrente. Questo può essere ottenuto usando la nuova classe UIImageAsset.
Inoltre se nell’Asset Catalog associamo una certa particolare immagine ad una determinata size class, la funzione UIImage(named:”nome_file”) automaticamente ritornerà l’immagine associata con la corrente class size dell’interfaccia.
Credo che siano rari i casi in cui vogliamo dare una immagine diversa per ogni possibile size class, ma ora ci è comunque possibile:
Codice
Il codice relativo a questo articolo può essere scaricato al seguente link:
Reference