In questa lezione del corso SwiftUI Tour vedrai che esiste un type chiamato Array che ci permetterà di rappresentare liste di oggetti che potremo manipolare e conservare all'interno di var e let.

Per il linguaggio Swift, un Array è un type generico che accetta sequenze ordinate di oggetti omogenei (ovvero che appartengono allo stesso type). Per esempio, in un'ipotetica app rubrica potremo creare un Array di oggetti String che rappresentano i nomi dei nostri contatti.

Grazie agli Array potrai anche gestire oggetti complessi o creati da te per rappresentare informazioni non gestite da type standard. Infatti, vedremo in questa lezione che potremo creare dei nuovi type per rappresentare oggetti come Recipe se stiamo sviluppando un'app di ricette, o Todo se stai sviluppando una todo-list app.

Nella prossima lezione, grazie alle nozioni imparate oggi, riuscirai a creare la tua prima Todo list app 🚀

Gli Array del linguaggio Swift vanno a braccetto con il componente List di SwiftUI. List permette di renderizzare row su singola colonna per ogni oggetto contenuto all'interno dell'Array.
Nell'esempio sopra, ho utilizzato il componente List dove per ogni Recipe viene creata una View che ne rappresenta il contenuto.

Ma basta con le introduzioni!

Partiamo dagli Array utilizzando il Playground e poi passiamo a SwiftUI.

Creare un Array in Swift

Un array del linguaggio Swift devi immaginarlo come un treno dove ogni vagone trasporta un oggetto. Ogni vagone è numerato e, di default, il primo vagone ha indice zero.

Puoi definire un array utilizzando due sintassi inferred ed explicit.

In maniera inferred o implicita puoi creare un Array utilizzando le parentesi quadre e separando gli oggetti da una virgola:

let arrayOfString = ["peppe", "giovanni", "emanuele", "emily"]
let arrayOfInt = [100, 200, 5, -40]

In maniera esplicita puoi inizializzare un oggetto di type Array<SomeType> specificando tra parentesi angolari il type di oggetti trasportati. Il costruttore da usare è arrayLiteral dove ogni elemento è separato da virgola:

let arrayExplicit = Array<String>(arrayLiteral: "peppe", "emily", "luca")
print(arrayExplicit)

💡 Come puoi notare la sintassi implicita è meno verbosa e generalmente più utilizzata.

Type Array

Nel caso in cui volessi esplicitare il type delle tue variabili/costanti, puoi utilizzare queste due sintassi equivalenti:

var contacts: [String] = ["Michael Scott", "Dwight Schrute"]
var contacts2: Array<String> = ["Michael Scott", "Dwight Schrute"]

Inizializzare Array vuoti

Spesso capiterà di voler creare array senza valori di partenza. In questo caso puoi utilizzare il costruttore senza valori o delle parentesi quadre vuote. Ricordati che dovrai però specificare il type degli oggetti contenuti:

var emptyIntArray: [Int] = []
var emptyBoolArray = Array<Bool>()

Accedere gli elementi di un Array

Dicevo che un array si comporta come un treno dove la prima carrozza ha indice 0. Gli index di un Array hanno valori Int che partono da 0 e a N-1 dove N è il numero di oggetti trasportati.

Per accedere ai vagoni utilizzando un index, ti basta far seguire al nome di una var/let un blocco di parentesi quadre con all'interno l'index. Più facile a farsi che a dirsi:

let latestTransactions = [100, -50, 70, -20]
latestTransactions[0] // 100
latestTransactions[1] // -50
latestTransactions[2] // 70
latestTransactions[3] // -20

Di conseguenza puoi usare questa sintassi per assegnare questi valori ad altre var o let:

let latestTransactions = [100, -50, 70, -20]
let firstTransaction = latestTransactions[0] 

print("your first transaction is", firstTransaction)

count, isEmpty, first e last

Prima di passare a vedere alcune funzioni che ti permetteranno di manipolare gli array, vediamo alcune proprietà che ci aiutano ad analizzare ed interagire con i suoi elementi.

Count

La prima è count che restituisce il numero di elementi presenti all'interno dell'array. count è una computed property ed, al suo interno, conta letteralmente quanti oggetti ci sono all'interno dell'array:

let planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]

planets.count // 8
print("there are \(planets.count) planets")

isEmpty

Qualora volessi capire se un array ha valori o meno puoi utilizzare un'altra computed property chiamata isEmpty. isEmpty restituisce un Bool in base alla presenza o meno di valori all'interno dell'array:

let todo: [String] = []

todo.isEmpty // true

if todo.isEmpty {
  print("Your todo list is empty")
}

let numbers: [Int] = [1]
numbers.isEmpty // false

Anche gli oggetti String dietro le quinte sono una forma speciale di Collection. Infatti, una String è una Collection di oggetti Character. Se vai nella doc ufficiale noterai che ci sono alcune proprietà e metodi in comune come isEmpty, count, append etc:

"Earth".isEmpty
"Earth".count

first e last. Accenno al type Optional

Infine, altre due computed property importanti degli Array sono le proprietà first e last che come puoi immaginare restituiscono il primo o l'ultimo elemento presente nell'array:

let planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]

planets.first // "Mercury"
planets.last // "Neptune"

Una cosa importante da notare è che queste due proprietà restituiscono un oggetto Optional. Un Optional, vedremo tra qualche lezione, è un type speciale che rappresenta la presenza o meno di valori (nil oppure un oggetto del type rappresentato).

Spesso i type optional li trovi seguiti da un ?, infatti se apri la doc di first e last noterai che il type è String?:

Un oggetto Optional devi ricordati che può rappresentare due oggetti:

  • Un oggetto speciale chiamato nil che viene restituito quando il valore è assente

  • Oppure un oggetto del Type rappresentato dall'Optional quando un valore è stato assegnato alla var/let.

var maybeInt: Optional<Int> = nil
maybeInt == nil // true

maybeInt = 2
maybeInt == nil // false

var maybeString: String? // equivalente di Optional<String>
maybeString?.isEmpty // nil

maybeString = "Test"
maybeString?.isEmpty // false

Interagire con questi oggetti richiede utilizzare il ? o eseguire un'operazione chiamata unwrap:

mercury?.count // use `?` to interact with an Optional

if let mercury {
    print(mercury) // now mercury is non-optional
}

La sintassi if let nomeVar permette di estrarre in maniera sicura il valore da un Optional, infatti l'if verrà eseguito solamente quando la var o let contiene un valore. In quest'ultimo caso, all'interno del branch o body dell'if sarai in grado di utilizzare il valore senza il simbolo ?.

Modificare gli elementi di un Array

Modificare un array in Swift ha delle regole che possono sembrare contro intuitive se non viste con un occhio informatico.

La prima nozione da ricordarsi è che nella maggior parte dei casi potrai modificare il valore contenuto da un Array solamente interagendo con il vagone che trasporta l'elemento da cambiare. La sintassi è:

nameArray[index] = newValue

Dove nameArray è il nome della variabile che contiene l'array, l'index è l'indice dell'elemento che vuoi modificare e newValue è il nuovo valore.

Per esempio, ipotizziamo di voler modificare una lista di contatti dove uno dei nomi ha un typo:

var contacts: [String] = ["Luca", "Gseppe", "Emily"]

Dato che il secondo elemento è quello con un errore, allora scriveremo:

contacts[1] = "Giuseppe"
print(contacts)
// ["Luca", "Giuseppe", "Emily"]

Se, invece, avessimo passato il valore di contacts[1] ad una nuova var e avessimo modificato quest'ultima, non avremmo modificato l'array ma semplice la copia passata alla var:

var contacts: [String] = ["Luca", "Gseppe", "Emily"]

var elementToEdit = contacts[1] 
elementToEdit = "Giuseppe" // ‼️ Does not change the array

print(contacts) // Array unchanged
// ["Luca", "Gseppe", "Emily"]

Accenni a Reference Type e Value Type

Il motivo per cui questa operazione non funziona risiede nel fatto che nel linguaggio Swift esistono due famiglie di tipi di dato: Reference Type e Value Type.

In maniera molto semplificata:

  • Reference Type: Possono essere modificati quando passati da una var all'altra. Reference infatti significa che stiamo passando un riferimento all'oggetto originale.

  • Value Type: Passare un valore da una var X ad una var Y clona il valore originale. Di conseguenza modificare Y non modificherà X.

Alla famiglia dei Value Type appartengono tutti i tipi di dato che vengono definiti utilizzando la keyword struct. La maggior parte dei type di base del framework foundation e di SwiftUI sono Value Type.
Infatti, se prendi la documentazione di String o Text ti accorgerai che sono type struct. Per esempio, anche Array è una struct. Di conseguenza passare il valore di array A ad array B e modificare B, non avrà effetto su A:

var arrayA = [5, 10, 15]
var arrayB = arrayA

arrayB[0] = 100 // has no effect on arrayA
print(arrayA) // [5, 10, 15]
print(arrayB) // [100, 10, 15]

Ho spiegato come accedere alla documentazione in questa lezione. Ricordati che puoi anche usare il Quick Help (CMD+Tap) su un Type per avere un'anteprima direttamente su Xcode.

Reference Type sono tipi di dato che vengono maggiormente rappresentati dalla keyword class. Spiegherò meglio la differenza tra queste due tipologie di type in una lezione dedicata.

Per il momento interagiremo maggiormente con oggetti nati da struct quindi è importante ricordarsi che passare valori da una var all'altra clonerà l'oggetto di partenza.

Aggiungere elementi da un Array

La funzione degli Array chiamata append permette di aggiungere in coda un nuovo elemento. La sintassi è la seguente:

var todo: [String] = []
todo.append("Completare il corso SwiftUI Tour")

Ovviamente, in base al type di array, cambierà la tipologia di valori che potrai appendere. Se hai un array di Int il metodo append accetterà oggetti Int e così via.

Nel caso in cui volessi aggiungere più di un elemento, per esempio volessi combinare due array tra loro, potrai utilizzare append(contentOf: someArray) oppure potrai utilizzare l'operatore di somma +:

var shoppingList: [String] = []
shoppingList.append("Coca cola")

// 1
shoppingList.append(contentsOf: ["Pasta", "Yogurt"])

// 2
let recurringItems = ["Water", "Bread"]
shoppingList.append(contentsOf: recurringItems)

// 3
shoppingList = shoppingList + ["Salad"]
print(shoppingList)

Nel caso in cui volessi appendere un array ad un altro, modificando il primo, ricordati di eseguire l'operazione di assegnazione come nell'esempio 3. Altrimenti stai ricreando un nuovo array che è la somma dei due.

Aggiungere ad una posizione specifica

Se volessi aggiungere un elementi ad una posizione ben precisa, potrai farlo utilizzando la funzione insert(Type, at: Int) dove nel parametro at passerai l'indice al quale vorresti aggiungere l'elemento. Di conseguenza spostando in avanti quello presente:

var steps: [String] = ["step 1", "step  3", "step 4"]

steps.insert("step 2", at: 1)

print(steps)
// ["step 1", "step 2", "step  3", "step 4"]

Index out of Range

Attenzione agli indici che utilizzi, infatti se l'indice non esiste la tua applicazione andrà in crash con il seguente errore: Fatal error: Array index is out of range.
Infatti, interagire con gli oggetti di un Array richiede conoscere a priori i loro indici.

In linea di massima, a meno che l'index non sia recuperato in maniera programmatica (vedremo tra poco come), è sconsigliato definire indici in maniera manuale.

Lo stesso errore lo ritroveremo nelle operazioni di rimozione.

Rimuovere elementi da un array

Le funzioni a disposizione per poter rimuovere elementi sono:

  • removeLast(): rimuove l'ultimo elemento dall'array.

  • remove(at: Int): Dove at sarà l'indice da rimuovere.

  • removeAll() Rimuove tutti gli elementi.

var steps: [String] = ["step 1", "step 2", "step  3", "step 4"]

steps.remove(at: 2)
// ["step 1", "step 2", "step 4"]

Ci sono poi variazioni di questi metodi come removeFirst(), removeFirst(Int) e removeLast(Int) che lascio a te il compito di capire come utilizzarli (qui la doc ufficiale).

Filter e Contains: Cercare elementi in un Array

Prima di passare a SwiftUI, diamo velocemente un'occhiata ad uno dei metodi più utilizzati per cercare elementi all'interno di un Array: filter e contains.

Il più utilizzato è sicuramente filter. Il metodo filter permette di estrarre da un Array un nuovo Array filtrato in base ad una condizione Bool. La sua definizione è alquanto complessa per le conoscenze che abbiamo adesso, ma possiamo comunque identificare alcuni concetti chiave:

func filter(_ isIncluded: (Self.Element) throws -> Bool) rethrows -> [Self.Element]

Infatti vediamo che:

  • isIncluded è una closure, ovvero una speciale funzione che, in questo caso, viene invocata su ogni element Self.Element e che vuole restituito un valore Bool.
    Ovvero possiamo applicare un'operatore condizionale che ci permetterà di capire se l'elemento deve essere estratto oppure no.

  • Ed infine, la funzione restituisce un -> [Self.Element] ovvero un Array degli elementi filtrati in base alla condizione isIncluded.

Vediamo in pratica come si utilizza. Immaginiamo di voler recuperare da un array di fruits tutti i frutti che cominciano per lettera a:

var fruits = ["apple", "banana", "orange", "grape", "kiwi", "ananas"]

let fruitsWithA = fruits.filter { value in
    value.hasPrefix("a")
}

print(fruitsWithA)

Nota come quella sintassi (_ isIncluded: (Self.Element) throws -> Bool) si è tradotta in:

{ value in 
  // some bool condition
}

Dove value è la costante che cattura ogni elemento dell'array. Nell'esempio ho utilizzato la funzione hasPrefix del type String che controlla se la prima lettera della stringa comincia per il valore passato tra parentesi. hasPrefix restituisce un Bool che poi viene utilizzato dalla funzione filter per filtrare gli elementi dell'array.

Nel caso in cui volessi solamente confermare la presenza di elementi all'interno di un array con un semplice true o false, potrai utilizzare il metodo: contains.

Esercizio 0. Verificare la presenza di elementi in un Array utilizzando il metodo contains.

Utilizza il metodo contains per cercare all'interno dell'array fruits la presenza di banana:

var fruits = ["apple", "banana", "orange", "grape", "kiwi", "ananas"]
  • Ricordati che puoi confrontare elementi utilizzando l'operatore ==.

Scrivimi sotto nei commenti in caso non riuscissi a trovare una soluzione.

Esercizio 1. Recuperare l'indice di un oggetto utilizzando il metodo firstIndex

Dato che la nostra app potrebbe crashare se non utilizziamo correttamente gli indici di un array, quest'ultimi forniscono diversi metodi per recuperare e cercare gli indici in maniera sicura.

Il primo si chiama firstIndex(of: Element) -> Int? e ci permette di recuperare l'index dell'elemento passato ad of. Se l'oggetto non esiste, la funzione restituirà nil (che rappresenta l'assenza di valore).

Ti faccio un esempio:

var fruits: [String] = []

fruits.append("Banana")
fruits.append("Apple")

fruits.firstIndex(of: "Apple") // 1
fruits.firstIndex(of: "apple") // nil

Ricordati che la comparazione tra stringhe è type sensitive, nel caso della ricerca di apple il metodo firstIndex restituisce nil perché l'elemento comincia per lettera maiuscola.

Puoi risolvere questi piccoli ma importanti problemi utilizzando il metodo firstIndex(where: (Self.Element) throws -> Bool) rethrows -> Int? che ti permette di scrivere una closure e specificare un metodo di ricerca custom. Nel nostro caso, potremmo trasformare i valori di ricerca in minuscolo, utilizzando il metodo lowercased():

var fruits: [String] = []

fruits.append("Banana")
fruits.append("Apple")

let indexOfApple = fruits.firstIndex { value in
    value.lowercased() == "apple"
}

Adesso che sai come utilizzare firstIndex, passiamo all'esercizio.

  • Recupera l'indice dell'elemento banana dall'array fruits ed eliminalo utilizzando remove(at: Int)

Ricordati che una volta recuperare l'indice con firstIndex (prova in tutti e due metodi) dovrai utilizzare la sintassi if let per accedere al valore non opzionale dell'indice restituito. Altrimenti non potrai passarlo al metodo remove dato che questo accetta un Int mentre il metodo firstIndex restituisce un Int?.

Nel caso di problemi, o se volessi qualche chiarificazione, fammi sapere nei commenti!

Conclusione

Grazie alla possibilità di rappresentare collezioni ordinate di elementi adesso possiamo finalmente introdurre il componente List di SwiftUI.

Infatti, sapere maneggiare gli Array e la maggior parte dei suoi metodi e proprietà è una condizione necessaria per poter comprendere appieno il componente List.

La prossima lezione sarà totalmente pratica e vedremo come creare una semplice todo list app in SwiftUI con funzionalità di aggiunta e rimozione di tasks.

Buona programmazione!