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 swipeActionsalle 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")
}
Come creare una List in swiftui

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")
    }
}
List e Section in SwiftUI

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 di List, 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:

  1. Una lista di elementi.

  2. Una proprietà da utilizzare come id.

  3. 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:

  1. Una sezione sempre visibile che permette l'inserimento di nuovo todo.

    1. Questa contiene al suo interno una TextField che ci permetterà di catturare l'input utente ed inserirlo in un array di task.

    2. Dopo che il task viene aggiunto, assicurati che il testo del Field venga resettato.

  2. Una sezione che rappresenta l'empty state quando l'utente non ha todo da completare.

  3. Una sezione con i todo da completare.

    1. Qui è dove inserirai il tuo ForEach per riempire la lista.

    2. Ogni row avrà le seguente action:

      1. leading edge: "Complete", un bottone per completare il task: ovvero rimuoverlo dalla lista e ed incrementare il contatore completedCount.

      2. trailing edge: "Delete", un bottone per eliminare il task ed incrementare il contatore deletedCount.

  4. 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 modifier onSubmit 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 al TextField 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 o var che ritornano delle some View. Ho spiegato come creare nuove view qui. Creando delle func 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 type Int 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.


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!