List e ForEach in SwiftUI
Nella lezione precedente abbiamo visto come il type Array
ci ha permesso di interagire con collezioni di elementi ordinati accessibili tramite un index
. Che sia l'app rubrica, il feed di YouTube, Instagram, o una semplice todo list app, gli array sono la base di tutte le applicazioni che rappresentano liste di elementi.
List
è l'oggetto che in SwiftUI ci permette di trasformare ogni singolo elemento di un Array
in una corrispettiva view. List
oltre a renderizzare elementi in una singola colonna, ci permette di associare swipeActions
e ci facilita in operazioni di selezione e ricerca di elementi.
In pratica List
è un componente tutto fare, da un lato gestisce liste potenzialmente infinite di elementi senza impattare troppo sulla performance e dall'altro lato aiuta noi sviluppatori in operazioni come swipe o ricerca che, se dovessimo ricrearle da zero, richiederebbero molte ore di sviluppo.
Oggi focalizzeremo l'attenzione principalmente su come utilizzare List
in combinazione con un Array
, vedremo che esiste un componente ForEach
che permette di creare List
più elaborate ed, infine, vedremo come aggiungere swipeActions
alle nostre view.
Imparerai tutto ciò sporcandoti le mani, infatti a fine di questa lezione sarai in grado di creare la tua prima todo list app:
Se sei capitato in questa lezione per sbaglio e vuoi imparare da zero come creare applicazioni per dispositivi Apple puoi seguire il corso SwiftUI Tour gratuitamente.
Ready?
Let's start!
Come creare una List statica in SwiftUI
Prima di passare alla todo list app dobbiamo imparare ad utilizzare il componente List
. Di baseList
può essere utilizzata come contenitore di views. Ogni view al suo interno verrà mostrata su singola riga separata dalle altre da un divisore:
List {
Text("Row 1")
Text("Row 2")
}
Di default tutti gli elementi vengono messi in una Section
comune, però nessuno ti vieta di definire delle altre per creare dei raggruppamenti a te più ideali:
List {
Section {
Text("Row 1")
} header: {
Text("Section A")
} footer: {
Text("Footer A")
}
Section {
Text("Row 1")
Text("Row 2")
Text("Row 3")
} header: {
Text("Section B")
} footer: {
Text("Footer C")
}
}
Queste tipologie di List
, ovvero liste che non sono associate ad Array dinamici, sono molto utili per creare pagine di contenuto statico che possono essere scrollate.
Ad esempio, List
è stata utilizzata nella pagina Settings di iOS. Se ci fai caso, li trovi una lista con gli elementi raggruppati in Section
.
Abbiamo incontrato il componente
Section
nella lezione su State e Binding. All'interno diList
,Section
funzionerà esattamente allo stesso modo.
Come creare List
dinamiche di elementi
Nel caso in cui volessi usare List
per renderizzare il contenuto di un Array
, puoi utilizzare il costruttore .init(_ data:, id:, rowContent:)
. Questo vuole come parametri:
Una lista di elementi.
Una proprietà da utilizzare come
id
.Una closure che ci permette, per ogni elemento, di renderizzare una view:
List(array, id: \.self) { element in
// some View
}
Per esempio, immaginiamo di avere un array di planets, potremo per ogni pianeta mostrare un Text
:
struct ContentView: View {
let planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
var body: some View {
List(planets, id: \.self) { planet in
Text(planet)
}
}
}
Identity di una View
In questa sintassi c'è un concetto che fino a questo momento non ho volutamente trattato e che prende il nome di View Identity.
In SwiftUI tutte le view hanno un id
che permette al motore grafico di capire quando queste sono state modificate e quando queste richiedono un aggiornamento o rimozione dalla scena.
Per la maggior parte delle view, SwiftUI riesce a dedurre l'id
in maniera autonoma eccetto quando utilizziamo List
e, tra poco vedremo, ForEach
.
Dato che stiamo utilizzando componenti che dipendono da informazioni esterne e che potrebbero collidere in identità (se per esempio ho due valori uguali nell'array), SwiftUI vuole esplicitamente definito quale proprietà utilizzare come id.
Ora la sintassi che ho utilizzato comincia per \
. Questo carattere permette di creare un oggetto di type KeyPath
. Un KeyPath
permette di puntare al nome di una proprietà di un Type
.
Ti faccio vedere un esempio solamente ai fini di capire cosa fa (ci ritorneremo in maniera dettaglia più avanti):
struct Person {
let name: String
let surname: String
var fullName: String {
"\(name)_\(surname)"
}
}
let giuseppe = Person(name: "Giuseppe", surname: "Sapienza")
let keyPathToFullName = \Person.fullName
Nell'esempio sopra il path della proprietà, aka var
o let
, a cui sto puntando è fullName
che è la combinazione tra name
e surname
.
In un'ipotetica List
dove utilizzo un array di oggetti Person
potrei utilizzare \.fullName
come id
di ogni View
(vedremo che esiste un sistema automatico e sicuro per definire id).
Ora, torniamo a List
.
Nell'esempio dei planets
, dato che l'array è di type String
, scrivere \.self
equivale a scrivere \String.self
.
Ma cos'è questo self
?
self
è una keyword del linguaggio Swift che sta ad indicare l'oggetto a cui stiamo facendo riferimento. Gli oggetti, per il linguaggio Swift, pur avendo stesso contenuto hanno identità diverse. Di conseguenza, SwiftUI li tratterà come view diverse.
ForEach e List
Utilizzare List
in combinazione con un Array
è molto limitativo e permette di risolvere solamente casi in cui vuoi mappare uno ad uno ogni elemento ad una view.
Ma come facciamo quando vogliamo mostrare sia view che dipendono da un Array
e contenuto indipendente?
Qui entra in gioco il componente ForEach
. Quando utilizziamo List
come semplice contenitore List {}
, possiamo aggiungere un ForEach
al suo interno che ci permetterà di iterare un Array
e mostrare del contenuto per ogni elemento.
Utilizzando List
e ForEach
possiamo aggiungere contenuto statico e dinamico all'interno di una singola List
. ForEach
ha la stessa sintassi vista poco fa per le List
+ Array
, ovvero:
let planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
var body: some View {
List {
ForEach(planets, id: \.self) { planet in
Text(planet)
}
}
}
Ora, immagina di avere un altro array con la lista delle lune del nostro sistema solare, potresti utilizzare un altro ForEach
all'interno della List
senza nessuna limitazione:
let planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
let famousMoons = ["Luna", "Phobos", "Deimos", "Ganymede", "Europa", "Io", "Callisto", "Titan", "Enceladus", "Triton"]
var body: some View {
List {
Section {
ForEach(planets, id: \.self) { planet in
Text(planet)
}
} header: {
Text("Planets")
}
Section {
ForEach(famousMoons, id: \.self) { moon in
Text(moon)
}
} header: {
Text("Moons")
}
}
}
Nota come ho innestato il ForEach
all'interno di una Section
per dividere il contenuto dei due array.
SwipeActions
Una delle caratteristiche che contraddistingue aggiungere elementi in una List
è quella di poter definire delle swipe actions utilizzando il modifier omonimo swipeActions
.
All'interno potrai aggiungere dei Button
che saranno mostrati quando l'utente esegue uno swipe da destra verso sinistra:
ForEach(planets, id: \.self) { planet in
Text(planet)
.swipeActions {
Button(action: {}, label: {
Text("Action A")
})
.tint(.orange)
Button(action: {}, label: {
Text("Action B")
})
.tint(.green)
}
}
Nota come ho usato il modifier
tint
per modificare il colore del bottone. In questo contesto SwiftUI colora l'intero bottone.
Nel caso in cui volessi mostrare le actions sulla sinistra, potrai aggiungere il parametro edge
che accetta uno dei valori del type HorizontalEdge
: leading
o trailing
(di default SwiftUI usa trailing
):
.swipeActions(edge: .leading) {
Ovviamente, nessuno ti vieta di poter aggiungere action su entrambi i lati:
.swipeActions(edge: .leading) {
}
.swipeActions(edge: .trailing) {
}
Esercizio 0. Come creare una todo list app in SwiftUI.
Adesso è arrivato il momento di mettere insieme sia le nozioni imparate oggi che quelle viste nelle lezioni precedenti.
Imparerai a creare una todo List app completando una serie di esercizi. Infatti, hai già tutte le skills per poter affrontare questo progetto in totale autonomia, io ti guiderò solamente in alcuni dei punti salienti.
Partiamo dalla UI e dai diversi requirements che dovrai soddisfare. In particolare dovremo creare una UI che contiene 4 sezioni:
-
Una sezione sempre visibile che permette l'inserimento di nuovo todo.
Questa contiene al suo interno una
TextField
che ci permetterà di catturare l'input utente ed inserirlo in un array di task.Dopo che il task viene aggiunto, assicurati che il testo del Field venga resettato.
Una sezione che rappresenta l'
empty state
quando l'utente non ha todo da completare.-
Una sezione con i todo da completare.
Qui è dove inserirai il tuo
ForEach
per riempire la lista.-
Ogni row avrà le seguente action:
leading
edge: "Complete", un bottone per completare il task: ovvero rimuoverlo dalla lista e ed incrementare il contatorecompletedCount
.trailing
edge: "Delete", un bottone per eliminare il task ed incrementare il contatoredeletedCount
.
Una sezione con statistiche come il numero di todo completati e cancellati.
Nota come non tutte le sezioni sono visibili e vengono renderizzate solamente al verificarsi di determinate condizioni:
Tips
Puoi catturare l'input utente senza dover inserire un bottone aggiungendo alla
TextField
il modifieronSubmit
che verrà attivato quando l'utente preme invio da tastiera:
TextField("...", text: $newTodoInput)
.onSubmit {
// do something with `newTodoInput
}
All'interno di
onSubmit
potrai utilizzare la variabile associate alTextField
per inserire il suo valore all'interno dell'Array che rappresenta i tuoi todo.Assicurati di inserire il task solamente quando la stringa non è
empty
.
Spezzetta l'interfaccia in piccoli componenti creando
func
ovar
che ritornano dellesome View
. Ho spiegato come creare nuove view qui. Creando dellefunc
all'interno della tua content view riuscirai ad avere un body più leggibile:
Per poter creare la sezione statistiche dovrai avere almeno due
State
di typeInt
che potrai incrementare quando l'utente esegue uno swipe da sinistra o destra.
Per eliminare un task dalla lista dei todo ricordati che puoi utilizzare il metodo
remove(at
che abbiamo incontrato nella lezione sugli Array.
Per poter mostrare e nascondere view, ricordati che puoi utilizzare
if
edelse
.
Se rimani bloccato nel completare questo esercizio, ricordati che è assolutamente normale. Eventualmente, fammi sapere tramite commenti.
Conclusione
Grazie agli array del linguaggio Swift ed al componente List
di SwiftUI siamo adesso in grado di rappresentare liste dinamiche di contenuto.
List
lo continueremo ad approfondire anche nel modulo successivo quando, finalmente, entreremo in contatto con concetti come navigazione, custom type, enum, ciclo for e funzioni.
Prima di passare al modulo successivo, ti invito vivamente a completare l'esercizio sopra perché ti permette di ripassare tutti i concetti affrontati in questo modulo.
Buona programmazione!