Array in Swift
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 diCollection
. Infatti, unaString
è unaCollection
di oggettiCharacter
. Se vai nella doc ufficiale noterai che ci sono alcune proprietà e metodi in comune comeisEmpty
,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 è assenteOppure 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 unavar Y
clona il valore originale. Di conseguenza modificareY
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 unType
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)
: Doveat
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 elementSelf.Element
e che vuole restituito un valoreBool
.
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 unArray
degli elementi filtrati in base alla condizioneisIncluded
.
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'arrayfruits
ed eliminalo utilizzandoremove(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!