Costruzione di View complesse

Abbiamo visto che SwiftUI richiede di esporre sempre un body che restituisce una View. Nella costruzione di View complesse, però, risulta necessario comporre queste Views con un determinato layout e restituirle sottoforma di un unico oggetto. Per fare questo, SwiftUI ci viene in aiuto attraverso il concetto di Content View

Gli oggetti base cui dialoghiamo, come Text che abbiamo visto nel capitolo precedente, sono aderenti al protocollo View. Esistono degli altri oggetti, più complessi, che nella dichiarazione delle loro API hanno un formato simile al seguente:

public struct VStack<Content> where Content : View 

Quello sopra è la dichiarazione del componente VStack che andremo ad analizzare nel corso dell'articolo. Se andiamo a vedere la definizione di Content, ci accorgeremo che:

@_functionBuilder public struct ViewBuilder {
    ...
    public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
    ...
}

In pratica, gli oggetti ti tipo VStack, utilizzano un costruttore ViewBuilder per comporre più elementi di tipo View insieme. 

La closure ViewBuilder

la closure viewbuilder ci permette di inserire istruzioni tipicamente dichiarative al suo interno: al posto di invocare un metodo addSubview, come facevamo con UIKit, inseriremo il listato di tutte le view contenute direttamente all'interno della closure.

Per capire meglio questo concetto, proviamo a dare un occhiata da vicino alla definizione per la struct VStack:


public struct VStack<Content> where Content : View {
    public init(
        alignment: HorizontalAlignment = .center, 
        spacing: Length? = nil, 
        @ViewBuilder content: () -> Content
    )
}

Il parametro content è definito come una closure () -> Content. Il compilatore conosce questo attributo, marcato implicitamente come @ViewBuilder, e sa come trasformarlo in una nuova closure che ritorna una view rappresentante tutto il contenuto all'interno di VStack.


Questo principio è presente in tutti gli oggetti che espongono una clausola where Content : View

Stack View

Una Stack View è una speciale View di tipo Container View che permette di restituire una composizione di altri oggetti View, secondo un layout verticale, orizzontale o di profondità.

Continuando i nostri paragoni con il framework UIKit, il paritetico corrispondente dell'oggetto che andremo ad analizzare in questo paragrafo, sono le UIStackView. In SwiftUI, però, a parte il concetto di fondo c'è molto poco delle UIStackView classiche.

Gli oggetti di tipo Stacks attualmente disponibili sono di tre tipi: HStack, VStack e ZStack. Rispettivamente: Stack Orizzontale, Stack Verticale e Stack di profondità (la Z indica la dimensione di profondità). 

Questi oggetti, insieme ai modificatori che abbiamo visto nel capitolo precedente, ci permettono di sostituire completamente il concetto di Auto Layout e Constraints. Da un punto di vista tecnico, questo aumenta notevolmente la responsività e la velocità di rendering dell'applicativo, perchè il sistema non ha bisogno di pre-calcolare nessun vincolo o layout specifico: tutto è dichiarato a priori, in pieno stile dichiarativo. 


figure

Fig.1 - Semplice gerarchia che illustra l'uso del contenitore VStack.

In codice, scriveremo qualcosa di questo tipo:


import SwiftUI

struct ContentView : View {
    var body: some View {
        VStack {
           Image( ... )
           Text( ... )
           Text( ... )
           Text( ... )
           Slider( ... )
        }
    }
}

Come si può vedere, a differenza di UIKit, non sono più presenti riferimenti a metodi come addSubview. Al suo posto, in SwiftUI, le viste sono create interamente con un processo di composizione dichiarativa delle strutture. Semplicemente, diciamo al compilatore che la nostra ContentView, ha al suo interno un contenitore VStack con dentro: Image, Text, Text, Text e Slider.

Non prestiamo attenzione, per ora, ai singoli costruttori. Avremo tempo per ritornare su immagini slider e controlli avanzati immediatamente avanti. Per ora, l'importante è capire esattamente cosa sta succedendo e perchè.

Una nota molto importante, relativamento a ViewBuilder, riguarda la sintassi da usare al suo interno. Anche se ci troviamo in presenza di Swift, infatti, all'interno di questa closure non è possibile usare alcuni operatori tipici a cui siamo stati abituati, tipo l'optional unwrapper:

...
VStack {
    if let anImage = anImage {
       Image( ... )
    }
    Text( ... )
    Text( ... )
    Text( ... )
    Slider( ... )
}

Questo codice produrrà un errore di compilazione. La closure ViewBuilder non è in grado - per ora - di interpretare correttamente l'optional unwrapping. Il seguente codice, invece, funziona correttamente:


...
VStack {
    if anImage != nil {
       Image( ... )
    }
    Text( ... )
    Text( ... )
    Text( ... )
    Slider( ... )
}

Costruiamo una TableViewCell

Visto che il modo più semplice per imparare un nuovo strumento è quello di andare alla pratica, in questa sezione proveremo a replicare il tipico comportamento di una UITableViewCell con titolo e sottotitolo. Il risultato che vorremmo ottenere è qualcosa di questo tipo:

figure

Fig.2 - La TableViewCell che vorremmo ottenere

La prima cosa da fare è creare un nuovo progetto che includa il supporto per SwiftUI:

newProject1
NewProject2

Sostituiamo adesso la struct ContentView fornita di default dal sistema con il seguente codice:


struct ContentView : View {
	var body: some View {
		VStack(alignment: .leading) {
			Text("Mario Rossi").font(.title)
			Text("Amministratore delegato").font(.subheadline).fontWeight(.thin)
		}
	}
}

A questo punto nel riquadro del Canvas di Preview, dovreste vedere apparire, magicamente il risultato delle vostre dichiarazioni:

FirstPreview

Fig.3 - La preview di Xcode11 che mostra il risultato delle nostre dichiarazioni al framework SwiftUI.

Se non doveste vedere nulla, accertatevi innanzitutto di essere in un ambiente macOS Catalina 10.15, il sistema di Live Preview, infatti, funziona esclusivamente su questo ambiente. Se siete già in ambiente 10.15 ma non vedete comunque la live preview, accertatevi di avere abilitato il pannello di Canvas come segue:


EnableCanvas

Fig.4 - Per fare fuzionare la Live Preview, accertatevi di essere in ambiente 10.15 e di avere abilitato il pannello Canvas.

Semplice? Sicuramente! Ci mancano ancora alcuni elementi per completare il nostro lavoro. La prima cosa è l'immagine. Scaricate l'immagine di riferimento che trovate in calce all'articolo ed, avendo cura di aver selezionato il folder Assets.xcassets del vostro progetto, trascinate al suo interno l'immagine appena scaricata.

DragImage

Fig.5 - Importiamo un'immagine per il nostro lavoro all'interno degli assets del progetto.

L'oggetto Image

Ogni framework che si rispetti, deve avere la possibilità di dialogare in maniera più o meno completa e complessa con le immagini. SwiftUI non è da meno ed offre un supporto veramente formidabile a questo tipo di oggetti che non ci farà rimpiangere neanche lontanamente il suo parente UIImageView.

Image è un oggetto di tipo View (il risultato è un oggetto aderente al protocollo View). La sua forma più elementare di uso è quella a cui forniamo il nome di una immagine da visualizzare:


Image("photoThumb")

Il risultato è quello di mostrare a schermo, all'interno di una View, l'immagine fornita come argomento. Se proviamo ad inserire questa istruzione all'interno della VStack:



...
VStack(alignment: .leading) {
    Image("photoThumb")
    Text("Mario Rossi").font(.title)
    Text("Amministratore delegato").font(.subheadline).fontWeight(.thin)
}
...

il risultato non sarà esattamente quello sperato:

FirstImageAttempt

Fig.6 - Il primo tentativo con Image non è proprio quello sperato.

Andiamo per ordine e cerchiamo di risolvere questo problema. Vedremo che è più semplice di quanto si possa immaginare.

La prima cosa da fare è allineare l'immagine in orizzontale con il resto della riga. Ricordate quando parlavamo di VStack? Il suo compito è quello di allineare in verticale la lista di elementi forniti nel suo ViewBuilder block. Appare evidente, dunque, che è necessario cambiare leggermente il diagramma delle viste su cui stiamo lavorando avendo cura di aggiungere un'ulteriore variante di Stack fornita da SwiftUI: HStack.

NewHierarchyLayout

Fig.7 - Il nuovo diagramma ad albero di come vorremmo modificare la nostra ContentView.

struct ContentView : View {
    var body: some View {
        HStack {
            Image("photoThumb")
            VStack(alignment: .leading) {
                Text("Mario Rossi")
                    .font(.title)
                Text("Amministratore delegato")
                    .font(.subheadline)
                    .fontWeight(.thin)
            }
        }
    }
}
SecondAttempt

Molto Meglio!

Ci resta da sistemare la forma e la dimensione dell'immagine. La prima cosa che voglio fare è dire alla nostra immagine che dovrebbe essere ridimensionabile (resizable) e mantenere il suo aspectRatio.


...
Image("photoThumb")
    .resizable()
    .aspectRatio(contentMode: .fit)
...

Poi vorrei darle una forma circolare e rimensionarla ad una dimensione imposta di 60pt:


...
.clipShape(Circle())
.frame(width: 60, height: 60, alignment: .center)
...

ed infine vorrei scostarla da sinistra di 20 punti


...
.padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 0))
...

Tutto insieme:


struct ContentView : View {
    var body: some View {
        VStack {
            HStack {
                Image("photoThumb")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .clipShape(Circle())
                    .frame(width: 60, height: 60, alignment: .center)
                    .padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 0))
                VStack(alignment: .leading) {
                    Text("Mario Rossi").font(.title)
                    Text("Amministratore delegato").font(.subheadline).fontWeight(.thin)
                }
            }
        }
        
    }
}

L'oggetto Spacer

A questo punto la nostra CustomTableView sembra essere completa ma persiste ancora un piccolo problema: sembra essere allineata al centro anzichè a sinistra. Il problema è legato al fatto che come comportamento base le Stack - ricordo che quella principale su cui stiamo lavorando si espande in orizzontale (HStack) -  sono create per prendere tutto lo spazio a disposizione sull'asse principale della Stack, dividendolo equamente tra i figli.

Dunque, per poter allineare a sinistra l'intero oggetto è necessario aggiungere un'ulteriore view neutra denominata Spacer. La View Spacer è una view flessibile che si espande automaticamente per tutto l'asse principale dello stack in cui è inserito. Aggiungiamo questa View, come ultimo elemento della nostra stack orizzontale:


struct ContentView : View {
    var body: some View {
        VStack {
            HStack {
                Image("photoThumb")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .clipShape(Circle())
                    .frame(width: 60, height: 60, alignment: .center)
                    .padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 0))
                VStack(alignment: .leading) {
                    Text("Mario Rossi").font(.title)
                    Text("Amministratore delegato").font(.subheadline).fontWeight(.thin)
                }
                Spacer()
            }
        }
        
    }
}

Ecco come apparirà la nostra preview, a lavoro completato:

FinalHierarchy

Fig 7a - La Gerarchia ad albero finale della CustomTableViewCell a fianco.

FinalPreview

Fig 7b - La preview sul canvas del risultato.

Nota: Nell'attuale versione di Xcode11, esiste un problema noto che non ridimensiona correttamente l'immagine quando si usa l'opzione .fit, ottenendo un'effetto di stretch sul contenuto.


Nel prossimo capitolo, vedremo alcune regole di buona condotta che prevedono di rendere il codice quanto più possibile minimale e compatto, adottando delle tecniche di estrazione delle subviews. Vedremo inoltre un'introduzione al passaggio dei parametri e come SwiftUI gestisce lo stato dei propri elementi.

Scarica il progetto del capitolo

Prossimo articolo: Introduzione agli stati in SwiftUI

© Copyright 2019 Sofapps - All Rights Reserved - VAT 05197020877