Programmare in SwiftUI: costruire view più complesse

Come costruire in SwiftUI una cella personalizzata con VStack, HStack, Image e Spacer, e come la closure ViewBuilder compone le view senza addSubview.

Read in English

Tradotto dall’inglese con l’aiuto dell’IA e rivisto dall’autore.Leggi l’originale

Una parete di legno azzurra con un pannello giallo, attraversata in verticale da una trave bianca

Abbiamo visto che SwiftUI richiede sempre di esporre un body che restituisce una View. Nella costruzione di view complesse, però, è necessario comporre queste View con un layout specifico e restituirle sotto forma di un unico oggetto. Per farlo SwiftUI ci viene in aiuto attraverso il concetto di Content View.

Gli oggetti di base con cui abbiamo a che fare, come Text che abbiamo visto nel capitolo precedente, adottano il protocol View. Ci sono altri oggetti, più complessi, che nella dichiarazione della loro API hanno un formato simile a questo:

Swift

public struct VStack<Content> where Content : View 

Quella qui sopra è la dichiarazione del componente VStack, che analizzeremo nel resto dell'articolo. Se guardiamo la definizione di Content, vedremo che:

Swift

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

In pratica, oggetti come VStack usano un costruttore ViewBuilder per comporre insieme più tipi di View.


La closure ViewBuilder

La closure ViewBuilder ci permette di inserire al suo interno istruzioni tipicamente dichiarative: invece di invocare un metodo addSubview, come facevamo con UIKit, inseriremo una lista con tutte le view contenute direttamente nella closure.

Per capire meglio questo concetto, guardiamo più da vicino la definizione della struct VStack:

Swift

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 riconosce questo attributo come implicitamente marcato con @ViewBuilder e sa come trasformarlo in una nuova closure che restituisce una view rappresentante tutto il contenuto dichiarato dentro VStack.


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


Stack View

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

Continuando i nostri confronti con il framework UIKit, l'oggetto corrispondente che analizzeremo in questo paragrafo è UIStackView. In SwiftUI, però, a parte l'idea di base, resta ben poco della classica UIStackView.

Gli stack attualmente disponibili sono di tre tipi: HStack, VStack e ZStack. Rispettivamente: Horizontal Stack, Vertical Stack e Depth Stack (Z indica la dimensione della profondità).

Questi oggetti, insieme ai modifier che abbiamo visto nel capitolo precedente, ci permettono di sostituire completamente il concetto di Auto Layout e Constraints. Dal punto di vista tecnico questo aumenta molto la reattività e la velocità di rendering dell'applicazione, perché il sistema non deve pre-calcolare nessun constraint o layout specifico: tutto è dichiarato a priori, in puro stile dichiarativo.

Una semplice gerarchia che mostra un tipico uso di VStack.
Fig.1: Una semplice gerarchia che mostra un tipico uso di VStack.

Nel codice scriveremo qualcosa del genere:

Swift

import SwiftUI

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

Come potete vedere, a differenza di UIKit, i riferimenti a metodi come addSubview non ci sono più. In SwiftUI le view si creano interamente con un processo di composizione dichiarativa delle struct. Semplicemente, diciamo al compilatore che la nostra ContentView contiene un container VStack che a sua volta contiene: Image, Text, Text, Text e Slider.

Per ora non ci preoccupiamo dei singoli metodi di creazione. Avremo modo di tornare tra poco sulle immagini e sui controlli avanzati come lo slider. Per ora l'importante è capire esattamente cosa sta succedendo e perché.

Una nota molto importante sul ViewBuilder riguarda la sintassi da usare al suo interno. Anche se siamo in presenza di Swift, dentro questa closure non è possibile usare alcuni operatori tipici a cui siamo abituati, come l'optional unwrapping:

Swift

...
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 il concetto di optional unwrapping. Il codice seguente, invece, funziona correttamente:

Swift

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

Costruire una TableViewCell personalizzata

Dato che il modo più semplice per imparare un nuovo strumento è metterlo in pratica, in questa sezione proveremo a replicare il comportamento tipico di una UITableViewCell con titolo e sottotitolo. Il risultato che vorremmo ottenere è qualcosa del genere:

La TableViewCell che vorremmo ottenere.
Fig.2: La TableViewCell che vorremmo ottenere.

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

I passi da seguireI passi da seguire
Fig.3: I passi da seguire

Ora sostituiamo la struct ContentView fornita di default con il codice seguente:

Swift
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 pannello Preview Canvas dovreste vedere comparire come per magia il risultato delle vostre istruzioni:

Il pannello Preview di Xcode 11 mostra il risultato delle nostre dichiarazioni al framework SwiftUI.
Fig.4: Il pannello Preview di Xcode 11 mostra il risultato delle nostre dichiarazioni al framework SwiftUI.

Se non vedete nulla, assicuratevi prima di tutto di lavorare su macOS Catalina 10.15: il sistema di Live Preview funziona esclusivamente in questo ambiente. Se siete già su 10.15 ma non vedete ancora la live preview, verificate di aver attivato il pannello Canvas come segue:

Perché la Live Preview funzioni bisogna essere su Catalina 10.15 e aver attivato il pannello Canvas.
Fig.5: Perché la Live Preview funzioni bisogna essere su Catalina 10.15 e aver attivato il pannello Canvas.

Semplice? Certo! Ci mancano ancora alcuni elementi per completare il lavoro. La prima cosa è l'immagine. Scaricate l'immagine di riferimento che trovate in fondo all'articolo e, dopo aver selezionato la cartella Assets.xcassets del vostro progetto, trascinateci dentro l'immagine appena scaricata.

Importare un'immagine per il nostro esercizio nella cartella degli asset.
Fig.6: Importare un'immagine per il nostro esercizio nella cartella degli asset.

L'oggetto Image

Ogni framework deve poter dialogare, in modo più o meno completo e complesso, con le immagini. SwiftUI offre un supporto davvero notevole per questo tipo di oggetti, che non farà rimpiangere nemmeno il suo cugino UIImageView.

Image è un oggetto View (il risultato è un oggetto conforme al protocol View). Nella sua forma più semplice, gli forniamo il nome di un'immagine da mostrare:

Swift

Image("photoThumb")

Il risultato è una View con l'immagine passata come argomento. Se proviamo a inserire questa istruzione dentro la VStack:

Swift

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

il risultato non sarà esattamente quello sperato:

Il primo tentativo con Image non è esattamente quello sperato!
Fig.7: Il primo tentativo con Image non è esattamente quello sperato!

Proviamo a risolvere il problema. La prima cosa da fare è allineare l'immagine in orizzontale con il resto del contenuto. Vi ricordate quando parlavamo di VStack?

Il suo compito è allineare verticalmente la lista di elementi forniti nel suo blocco ViewBuilder. È chiaro, quindi, che bisogna modificare leggermente lo schema delle view su cui stiamo lavorando, aggiungendo un'ulteriore variante di Stack fornita da SwiftUI: HStack.

Il nuovo schema ad albero di come vorremmo modificare la nostra ContentView.
Fig.8: Il nuovo schema ad albero di come vorremmo modificare la nostra ContentView.
Swift

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)
            }
        }
    }
}
Secondo tentativo
Fig.8a: Secondo tentativo

Molto meglio!


Dobbiamo ancora sistemare la forma e la dimensione dell'immagine. La prima cosa da fare è dire alla nostra immagine che deve essere ridimensionabile e mantenere il suo aspectRatio.

Swift

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

Poi vorrei darle una forma circolare e forzarne la dimensione a 60pt:

Swift

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

e, infine, vorrei aggiungere un po' di padding a sinistra, diciamo 20 punti:

Swift

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

Tutto insieme:

Swift

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 completa, ma resta un piccolo problema: sembra allineata al centro invece che a sinistra.

Il problema dipende dal fatto che gli stack, per impostazione predefinita (ricordo che quello su cui stiamo lavorando si espande in orizzontale, HStack), sono pensati per occupare tutto lo spazio disponibile sul loro asse principale, dividendolo in parti uguali tra i figli.

Quindi, per allineare tutto l'oggetto a sinistra, bisogna aggiungere un'ulteriore view neutra chiamata Spacer. La View Spacer è una view flessibile che si espande automaticamente lungo tutto l'asse principale dello stack in cui viene inserita. Aggiungeremo questa View come ultimo elemento del nostro stack orizzontale:

Swift

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:

La gerarchia ad albero finale della CustomTableViewCell.La gerarchia ad albero finale della CustomTableViewCell.
Fig.9: La gerarchia ad albero finale della CustomTableViewCell.

Nota: l'attuale versione di Xcode 11 contiene un problema noto che non ridimensiona correttamente l'immagine quando si usa l'opzione .fit, producendo invece una sorta di effetto di stiramento del contenuto.

Nel prossimo capitolo vedremo alcune regole di buona condotta per rendere il codice il più minimale e compatto possibile, adottando tecniche di estrazione delle subview. Vedremo anche un'introduzione al passaggio dei parametri e a come SwiftUI gestisce lo stato dei suoi elementi.

-- 🖖🏻

Continua a leggere

Tutti gli articoli
  1. Programmare in SwiftUI: ObjectBinding e BindableObject
  2. Programmare in SwiftUI: l'oggetto View
  3. Programmare in SwiftUI: introduzione
↑ ↓ naviga↵ apriesc chiudi