Come creare nuove View in SwiftUI
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 unprotocol B
diremo che ilType A
implementa ilprotocol B
. OppureType A
conforms toprotocol 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 laView
sarà una o più linee di testo. Oppure, il typeImage
conforms toView
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 nuoviType
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 uninit
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.
-
Nel tuo
File Navigator
, fai tasto destro sulla cartella principale e selezionaNew Group
. Chiama questa cartellaComponents
.Un primo sistema per organizzare i progetti è quello di avere una cartella
Components
dove salvare tutte le view riutilizzabili.
Adesso, tasto destro sulla cartella
Components
e selezionaNew File
. Da qui, selezionaSwiftUI View
e chiama questo nuovo fileImageCard
.
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 dueinit
:
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 aggiungoreturn
. Dovrai sempre aggiungere ilreturn
della tuaView
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:
Spezzare interfacce complesse in piccoli componenti riutilizzabili.
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!