Nella lezione precedente abbiamo visto come utilizzare var, let e @State all'interno di una View.

Ma, fino a questo momento, non abbiamo mai spiegato cos'è effettivamente una View, come funziona quel var body e come sia possibile crearne di nuove.

Un'app medio grande è composta da migliaia righe di codice ed, ovviamente, è impossibile pensare di scrivere tutto all'interno della ContentView. Infatti, una delle skills essenziali per poter creare app complesse è quella di sapere spezzare le proprie interfacce in tanti piccoli componenti riutilizzabili.

Come regola generale, ricordati che più codice scrivi all'interno di un file più sarà complesso per te ed i tuoi colleghi comprenderne il significato.

Quindi, senza ulteriori presentazioni, partiamo dal comprendere cos'è una View.

Let's start!

Accenni ai Protocol

Se un Type è la fabbrica che usiamo per costruire i nostri oggetti, un protocol o interfaccia definisce in maniera astratta quali proprietà o funzionalità un Type dovrà implementare.

I protocol vengono principalmente usati per descrivere data e behaviour, ovvero var, let e func, di Type eterogenei ma legati da un filo conduttore.

Per esempio, immaginiamo di voler creare un gioco in cui il nostro player dovrà affrontare diversi mostri.
Un protocollo chiamato Monster ci potrà aiutare a definire le funzionalità comuni tra tutti i mostri, health, armor, strength etc mentre i Type che lo implementeranno definiranno concretamente i valori: esempio gli oggetti di type Bug avranno armor = 50 mentre tutti gli Zombie avranno armor = 20.

Per tornare a concetti a noi più vicini, abbiamo già incontrato i protocol.

Infatti tutti gli oggetti grafici di SwiftUI, come il Text, VStack, Image etc sono Type che implementano il protocollo View. Il protocollo View, vedremo nel dettaglio tra un attimo, definisce le proprietà comuni a tutti gli oggetti grafici.

Quando un Type A utilizza un protocol B diremo che il Type A implementa il protocol B. Oppure Type A conforms to protocol B.

Vediamo concretamente dove sta la differenza.

Prendiamo in esempio il type Text di SwiftUI ed entriamo nella documentazione.

Una delle prime cose che ci viene detta è che Text è una view che renderizza un o più linee di testo. Le parole in programmazione non vengono mai usate a caso.

Infatti dire che Text is a view that... è un riferimento al fatto che il type Text implementa il protocol View.

Come lo so per certo?

Se scendi in fondo alla documentazione vedrai che tra le Relationship troverai la parola View. Infatti il type Text conforms to View.

Entriamo adesso all'interno della documentazione del protocol View.

Il protocol View definisce type che rappresentano parte della nostra interfaccia.

Ti ho detto che un protocol è una rappresentazione astratta ed infatti, l'implementazione del protocollo spetta al Type che lo implementa.

Il type Text definisce concretamente che la View sarà una o più linee di testo. Oppure, il type Image conforms to View e serve a rappresentare immagini remote o locali.

Come faccio a sapere quali type implementano il protocollo view?

Se adesso scendi in fondo alla lista delle Relationships troverai un paragrafo chiamato Conforming Types che elenca tutti i type di SwiftUI che implementano il protocol View:

I requisiti del protocollo View

Se scendiamo nei dettagli del protocollo View, notiamo come l'unica regola da rispettare è che i Type che lo implementano dovranno esporre una computed property chiamata body che permetterà a SwiftUI di comporre la UI.

some View

Adesso che sappiamo che ogni view come Text, VStack etc è una View specializzata, possiamo finalmente dare una semplice spiegazione a some View.

some View l'abbiamo incontrato nella definizione della var body del protocollo View:

var body: some View {
    VStack {
        Image(systemName: "globe")
            .imageScale(.large)
            .foregroundStyle(.tint)
        Text("Hello, world!")
    }
    .padding()
}

Tradotto letteralmente some View significa un type conforme al protocollo View.

Swift sappiamo che è un linguaggio type-safe, in questo caso il protocollo costringe la var body nel specificare il type della view che SwiftUI dovrà renderizzare su schermo.

Il problema è che ci sono troppe View e, soprattutto, i modifiers o le view contenitori come VStack etc complicano il type di una view.

Ti faccio un esempio. Se prendiamo questo pezzo di codice:

VStack {
    Image(systemName: "globe")
    Text("hello")
    Text("world")
}

Il suo type generato è VStack<TupleView<(some View, Text, Text)>>.

Ciò significa che il più complicheremo la nostra body più il type si complica:

Per questo motivo il linguaggio Swift offre la keyword some per semplificare il Type restituito da funzioni o variabili.
Agli occhi del programmatore sappiamo che ritorneremo una some View ma il compilatore saprà calcolare il Type reale dietro quel some.

Da qui in avanti utilizzeremo some View per la maggior parte delle nostre var o func che genereranno View, in questo modo non dovrai preoccuparti di calcolare il type reale.

Creare nuove View in SwiftUI

Apri un qualsiasi progetto SwiftUI e soffermiamoci ad analizzare la linea struct ContentView. Qui, dopo i due punti : troviamo un riferimento al protocollo View.

struct ContentView: View {

Infatti, diremo che ContentView conforms to View o semplicemente ContentView è una View.

struct è una keyword del linguaggio Swift che permette di definire nuovi Type all'interno del nostro progetto. Nel prossimo modulo vedremo meglio come fare.

Dicevo che i protocol definisco dei requisiti, questi nel caso della View è quello che ogni implementazione deve implementare una proprietà chiamata body.

Infatti, se cancelli o rinomini la var body in var bodyTest vedrai che Xcode ti segnalerà un errore:

L'errore Type 'ContentView' does not conform to protocol 'View' sta ad indicare che la definizione del Type non sta rispettando i requisiti del protocollo View.

La ContentView, o in generale qualsiasi nuovo type che implementa View, deve obbligatoriamente esporre la var body.

Capito che struct serve a definire nuovi type e che View è il protocollo da implementare, nessuno ci blocca dal creare nuovi type o nuove view.

Subito dopo le parentesi graffe della struct ContentView, creiamo una nuova view chiamata MyFirstView.

struct MyFirstView: View {
    
    var body: some View {
        
    }
    
}

I nomi di Type vanno sempre scritti con prima lettera maiuscola. Non è un errore ma una regola che viene tramandata in tutti i linguaggi ad oggetti.

Xcode ci segnala un errore, infatti la var body deve restituire un valore. Per il momento, aggiungiamo un semplice Text("Hello from MyFirstView").

Dato che MyFirstView è un nuovo type possiamo inizializzarlo utilizzando init o ().

Gli oggetti che nascono da type di tipo struct espongono un init di default che non accetta parametri. Questo viene automaticamente creato da Swift a compile time. Tra poco vedremo come esplicitare l'init all'interno dei nostri type.

Dove lo creiamo ed aggiungiamo?
Mettiamolo all'interno della ContentView body:

Ovviamente nessuno ti vieta di creare più di un oggetto per quel type. Esattamente come per Int, Text o qualsiasi altro type, anche per quelli creati da te, non ci sono limiti al numero di oggetti creati.

Per il momento questo nuovo type MyFirstView è abbastanza inutile. Vediamo un esempio più complesso.

Dividere l'interfaccia in componenti riutilizzabili

Uno dei benefici di creare nuovi type è quello di nascondere implementazioni complesse e facilitare il riutilizzo di codice.

Infatti, immaginiamo di voler creare un feed di immagini dove ogni immagine ha uno style di default:

Dato che questo style verrà utilizzato in più parti della tua app (HomeView, ProfileView, FavouritesView etc), potremo creare un nuovo type chiamato ImageCard che prende in input un URL e, nel suo body, definisce l'immagine ed animazione di caricamento.

Vediamo come fare.

Creare un nuovo file

Il primo step è quello di creare un nuovo file dove conservare questa nuova view.

  1. Nel tuo File Navigator, fai tasto destro sulla cartella principale e seleziona New Group. Chiama questa cartella Components.

    1. Un primo sistema per organizzare i progetti è quello di avere una cartella Components dove salvare tutte le view riutilizzabili.

  2. Adesso, tasto destro sulla cartella Components e seleziona New File. Da qui, seleziona SwiftUI View e chiama questo nuovo file ImageCard.

Adesso che hai questo nuovo type, puoi creare i suoi oggetti all'interno di qualsiasi view nel tuo progetto.

Infatti se vai all'interno della ContentView puoi aggiungere ImageCard al suo body:

Crea nuovi init e passare valori ad una View

L'init o costruttore di un oggetto è uno dei modi migliori per passare valori dall'esterno. Infatti abbiamo già visto in azione questo sistema quando abbiamo passato String dentro Text con Text("una stringa") or Image con URL o il nome di una icona.

Per creare un costruttore custom, dovrai definire manualmente l'init all'interno dei tuoi type. La sintassi è la seguente:

init(parameter: Type) {
   // do something with parameter
}
  • init fa capire a Swift che stai creando un nuovo costruttore per quel type.

  • Dentro le parantesi tonde aggiungerai i tuoi parametri o argomenti. La sintassi è name: Type. Esattamente come definire una variabile.

    • Nel caso in cui volessi aggiungere più parametri, ti basterà dividerli da una virgola: init(parameterA: String, parameterB: Int)

  • Dentro le parantesi graffe possiamo catturare il valore passato all'argomento per poi passarlo a qualche variabile/costante interna al nostro type.

Applichiamo questo concetto al type CardImage e definiamo un init che accetta un URL:

struct ImageCard: View {
    
    init(url: URL) {
        
    }
    
    var body: some View {
        Text("Hello, World!")
    }
}

Generalmente gli init vengono definiti nella parte superiore della definizione del type.

A questo punto, notiamo che Xcode ci segnala un errore nella Preview: Missing argument for parameter 'url' in call.

Infatti, quando definisci manualmente un init, stai sovrascrivendo quello generato di default Swift.

Come risolviamo il problema?

  • O aggiungi un nuovo init che accetta zero argomenti. Quindi ti ritroveresti con due init:

struct ImageCard: View {
    
    init(url: URL) {
        
    }
    
    init() {
        
    }
  • Oppure devi obbligatoriamente aggiornare tutte le istanze in errore.

Non sempre è conveniente aggiungere l'init con zero parametri. Infatti, in alcuni contesti, può complicare la logica delle tue View.

In questo caso ti consiglio di risolvere i problemi segnalati da Xcode. Nel nostro caso, ci basterà usare un URL a caso:

#Preview {
    let url = URL(string: "https://www.peppe.app/content/images/2023/10/kangaroo.jpg")!
    
    return ImageCard(url: url)
}

Nota come ho definito una let ed ho aggiungo return. Dovrai sempre aggiungere il return della tua View d'esempio quando definisci del codice all'interno del blocco #Preview.

Adesso che siamo in grado di passare un valore al costruttore, vediamo come utilizzarlo.

Gli argomenti di un init si comportano come let, di conseguenza potrai utilizzare i loro valori scrivendo il nome dell'argomento all'interno delle parentesi graffe.

Una regola importante: Gli argomenti di un init, ma vedremo lo stesso varrà per altre sintassi, possono essere utilizzati solamente all'interno dello scope in cui sono stati definiti. Dove per scope si intende le parentesi graffe {}.

Quindi come passiamo il valore dall'init al body della View?

Qui ci aiutano le var e let viste nella lezione precedente.

In questo caso possiamo creare una let chiama imageURL all'interno dello scope della ImageCard e poi assegnare l'argomento url alla imageURL:

struct ImageCard: View {
    
    let imageURL: URL
    
    init(url: URL) {
        imageURL = url
    }

Spesso in Swift si utilizza dare lo stesso nome degli argomenti del costruttore alle proprietà del type. In questo caso, dovrai utilizzare la sintassi self.propertyName per differenziare l'argomento dalla proprietà del type. Esempio:

 struct ImageCard: View {
    
    let url: URL
    
    init(url: URL) {
        self.url = url
    }

Grazie al self Swift capisce che ti stai riferendo alla proprietà all'interno dello scope del type.

Argomenti senza label

Spesso definire il nome dell'argomento durante la costruzione dell'oggetto potrebbe risultare ridondante.

Per esempio init(url: URL) richiede esplicitare url quando costruiamo l'oggetto: ImageCard(url: myImageURL). Ovviamente già guardando il type che accetta il costruttore sappiamo che dovremo passare un URL, quindi la presenza del nome dell'argomento è ridondante.

Per omettere la label nel momento dell'uso del costruttore, e poi vedremo funzioni, ti basterà aggiungere, alla definizione, il simbolo underscore _ prima del nome dell'argomento:

init(_ url: URL)

In questo modo, all'utilizzo dovrai solamente passare il valore o una var/let che contiene un URL.

let url = URL(string: "https://www.peppe.app/content/images/2023/10/kangaroo.jpg")!
ImageCard(url)

Da notare come questa funzionalità di Swift è utilizzata in molti costruttori, per esempio molti degli init del type Text cominciano per _. Ovvero la prima label può essere omessa:

Esercizio. Completa la definizione di ImageCard

Adesso che hai visto come catturare valori da poter utilizzare all'interno del tuo type, completa l'esercizio usando la proprietà url all'interno del body utilizzando la AsyncImage.

Tip: Se non ti ricordi come utilizzare la AsyncImage, l'ho spiegata in questa lezione.

Conclusione

Creare nuove definizioni di view serve generalmente a risolvere due problemi:

  1. Spezzare interfacce complesse in piccoli componenti riutilizzabili.

  2. Organizzare il layout e navigazione della tua app. Infatti spesso ti ritroverai ad avere view principali come per esempio HomeView, ProfileView, CheckoutView etc che a loro interno utilizzano i diversi componenti che hai creato.

Per esempio, sotto abbiamo una HomeView dove il body renderizza HeaderView, ReminderView e FriendsList chiaramente view create appositamente per il nostro progetto:

struct HomeView: View {
    
    var body: some View {
        ScrollView {
            HeaderView()
            ReminderView()
            FriendsList()
        }
    } 
}

Se hai avuto qualche problema, al solito lascia un commento qui sotto.

Buona programmazione!