Il type Optional in Swift
Tutti i linguaggi di programmazione ad oggetti, e non solo, dispongono di un sistema che permette di rappresentare l'assenza momentanea o permanente di valori.
Per il linguaggio Swift questa dualità è rappresentata dal type Optional
. Un oggetto di type Optional
è un contenitore generico che può trasportare qualsiasi type e dove l'assenza di quest'ultimo è descritto dal valore speciale nil
.
Oggetti Optional
sono particolarmente utili quando hai bisogno di definire variabili senza valore di "partenza": immagina, per esempio, che la tua app permetta di selezionare un condimento opzionale per la tua pizza. Grazie al type Optional
potrai creare una var
senza valore di partenza, ovvero nil
, che eventualmente verrà o meno riempita in base alla preferenza dell'utente.
Un opzionale viene definito utilizzando due sintassi principali, esplicita ed implicita:
// Explicit
var age: Optional<Int> = nil
print(age) // nil
age = 30
print(age) // Optional(30)
// Implicit
var name: String? = nil
name = "Giuseppe"
In linea di massima incontrerai tipi di dato opzionali definiti utilizzando il simbolo ?
questo ti farà capire che l'oggetto contenuto all'interno di quella var
o let
potrà essere nil
oppure del type in questione.
Per fare un esempio lampante, prendiamo in considerazione il metodo firstIndex(where:
degli array. Se guardiamo la signature della funzione vediamo come questa restituisce un Optional<Type>
o Type?
. Infatti ritornerà il valore nil
nel caso in cui l'elemento cercato non fosse presente:
let fruits = ["apple", "banana", "kiwi"]
let ananas: String? = fruits.first(where: { $0 == "ananas" })
print(ananas) // nil
Adesso che sappiamo riconoscere oggetti di tipo opzionale ed a cosa servono, scendiamo nei dettagli di come utilizzarli e sfruttarli nei nostri progetti.
Let's start!
Force Unwrap, estrarre opzionali brutalmente
Spesso capiterà di voler estrarre il valore da un oggetto opzionale nel caso in cui questo esistesse.
Ipotizziamo, per esempio, di avere un server che restituisce alla nostra app un'offerta da mostrare ai nostri utenti. Immaginiamo d'avere una funzione chiamata getDiscount()
che restituisce un Int?
.
func getDiscount() -> Int? {
// some complex API request
// let's mimic the API response by returning a random value
return Bool.random() ? Int.random(in: 10...30) : nil
}
Nell'esempio ho utilizzato
Bool.random()
eInt.random(in:
per simulare uno scenario in cui un server ci restituisce o meno dei risultati.In questo modo ogni qual volta utilizziamo la funzione troveremo dei risultati diversi. Avremo un numero nel caso in cui
Bool.random()
restituiscetrue
onil
sefalse
.
Adesso utilizziamo la getDiscount
per mostrare o meno il valore di sconto all'interno della nostra UI. Come facciamo?
Ci sono due strategie, la prima sarebbe quella di provare ad estrarre forzatamente il valore dall'opzionale utilizzando la sintassi chiamata Force Unwrap.
Puoi eseguire un force unwrap utilizzando il simbolo !
subito dopo il valore:
let name: String? = "Peppe"
name // Optional("Peppe")
let nameUnwrapped = name! // "Peppe"
Grazie al !
puoi estrarre brutalmente il valore dall'optional, di conseguenza trasformando quel nuovo oggetto nel type contenuto. Nell'esempio la costante name
è un Optional
mentre nameUnwrapped
è String
.
Ma, cosa succede quando proviamo ad estrarre il valore da una variabile con valore nil
?
let age: Int? = nil
let ageUnwrapped = age! // ERROR: Crash
La tua applicazione crasha con errore Fatal error: Unexpectedly found nil while unwrapping an Optional value
. Ovvero hai provato ad eseguire un unwrap di un valore nil
:
Se ti stai chiedendo, beh ma io so in anticipo se un valore è opzionale o meno, quindi posso prevenire questo errore.
Purtroppo non è sempre così.
Infatti, se riprendiamo l'esempio in cui questi valori sono restituiti da sistemi terzi come un'API o delle funzioni, non saprai mai cosa ti potrebbero ritornare. Nel caso della getDiscount()
che abbiamo definito sopra, ti basterà eseguire il codice un paio di volte per renderti conto che potrebbe far crashare l'app in maniera random:
func getDiscount() -> Int? {
// some complex API request
return Bool.random() ? Int.random(in: 10...30) : nil
}
let discountA = getDiscount()!
Nota come sono stato in grado di utilizzare il
!
dopo l'invocazione della funzione. In pratica ho eseguito un force unwrap del valore restuito dallafunc
.
Come possiamo trattare questi valori in maniera sicura?
Qui entra in gioco l'optional unwrap.
Optional Unwrap, estrarre opzionali con sicurezza
Sfruttando l'istruzione if
potremmo controllare che il valore non sia nil
prima di utilizzare il force unwrap:
let age: Int? = nil // Change this value
if age != nil {
print("age:", age!)
} else {
print("age is nil")
}
Quest'operazione però richiede un po' di cerimonie, per fortuna esiste una sintassi chiamata Optional Unwrap che permette di eseguire un if
e contemporaneamente estrarre il valore:
let age: Int? = nil
if let age {
print("age:", age)
} else {
print("age is nil")
}
Grazie ad if let
potrai catturare ed estrarre il valore di un opzionale in maniera sicura. All'interno del branch la costante catturata sarà non-optional.
Nel caso in cui ti servisse un optional unwrap di una funzione oppure vuoi cambiare il nome della costante, potrai utilizzare la sintassi if let unwrappedName = someOptional
:
let fruits = ["apple", "banana", "kiwi"]
if let index = fruits.firstIndex(of: "banana") {
print("banana index:", index)
}
// ------ //
func getDiscount() -> Int? {
// some complex API request
return Bool.random() ? Int.random(in: 10...30) : nil
}
if let discountA = getDiscount() {
print("discountA:", discountA)
}
if let discountB = getDiscount() {
print("discountB:", discountA)
}
Nil Coalescing
Nel caso in cui vuoi eseguire un unwrap oppure, nel caso nil
, volessi utilizzare un valore di default puoi utilizzare la sintassi optional ?? defaultValue
chiamata nil-coalescing:
import SwiftUI
var selectedColor: Color?
let backgroundColor = selectedColor ?? .primary
Nell'esempio se non esiste un valore di selectedColor
, la costante backgroundColor
utilizzerà il valore .primary
.
La sintassi nil-coalescing è utilissima in SwiftUI quando modifiers richiedono valori non opzionali e devi gestire input/valori opzionali ed al tempo stesso definire parametri di default.
E dato che stiamo parlando di SwiftUI, vediamo alcuni edge-case di opzionalità.
Optional type in SwiftUI
In SwiftUI, utilizzare @State
di tipo opzionale richiede dei piccoli accorgimenti nel caso in cui volessi eseguire dei Binding
con componenti come TextField
.
Infatti, se guardiamo tutti i costruttori del TextField
non esiste nessun Binding<Type?>
. Ovvero non accetta binding opzionali.
Quindi come facciamo?
In questi casi dovrai costruire un Binding
utilizzando il costruttore Binding(get: () -> Value, set: (Value) -> ())
. Dove troverai due closure:
get
ti permetterà di catturare il valore dello@State
da mostrare nel Field.set
ti permetterà di catturare il valore inserito nel Field per poi passarlo al tuo@State
.
Ovvero potrai scrivere:
@State var name: String? = nil
var body: some View {
Form {
TextField(
"Name",
text: Binding(
get: { name ?? "" },
set: { name = $0 }
)
)
}
}
Nota come nella closure get
ho utilizzato l'operatore di nil-coalescing per restituire un valore String
nel caso in cui name
fosse opzionale. Mentre nel set
ho utilizzato il parametro anonimo $0
che rappresenta il valore inserito all'interno del field (equivalente di { value in name = value }
).
Sarà raro che definirai @State
con valori opzionali, ma è comunque importante saper definire Binding
in maniera esplicita senza utilizzare la sintassi $state
.
Continuando su questa falsa riga, immaginiamo di voler cambiare il colore di tint
dei nostri bottoni. Potremmo aggiungere uno @State var selectedColor: Color? = nil
ed utilizzare il componente ColorPicker
:
ColorPicker(
"Color",
selection: Binding(
get: { selectedColor ?? .black },
set: { selectedColor = $0 }
))
Button("Some Button") {
}
.tint(selectedColor ?? .accentColor)
Picker e valori opzionali
Il componente Picker
, ovvero l'oggetto che ci permetterà di definire campi di selezione, è probabilmente il componente che ti permetterà di capire fino in fondo le potenzialità dei type Optional
.
Ipotizziamo d'avere una UI che ci permette di selezionare un condimento aggiuntivo per la nostra pizza. Di conseguenza, nella nostra view definiremo una let
con la lista dei toppings
disponibili ed una @State var selectedTopping: String?
che conterrà la selezione utente:
let toppings = ["Mozzarella", "Funghi Porcini", "Rucola", "Salame"]
@State var selectedTopping: String?
Per inizializzare un Picker
puoi utilizzare l'init
che espone il binding selection
e due closure: content
che ti permette di definire la lista degli elementi da mostrare ed una label
accessoria:
Picker(selection: Binding(
get: { selectedTopping ?? "" },
set: { selectedTopping = $0 }
)) {
ForEach(toppings, id: \.self) { topping in
Text(topping)
}
} label: {
Text("Topping")
.font(.subheadline)
}
In questo momento però, se selezioni un valore dal Picker
noterai che questo non viene propagato all'interno dello @State selectedTopping
. Infatti, per farlo, dovrai aggiungere il modifier tag
ad ogni elemento della lista.
Il modifier tag
permette a SwiftUI di associare il valore con la view che lo rappresenta. Nel nostro caso passeremo lo stesso valore visualizzato. In questo modo, quando l'utente selezionerà il valore, questo verrà passato allo state.
Infine, possiamo aggiungere un Text
che rappresenta il valore nil
utilizzando, come valore del tag
, la stringa vuota ""
:
Picker(selection: Binding(
get: { selectedTopping ?? "" },
set: { selectedTopping = $0.isEmpty ? nil : $0 }
)) {
ForEach(toppings, id: \.self) { topping in
Text(topping)
.tag(topping) // IMPORTANT!!
}
Text("")
.tag("")
} label: {
Text("Topping")
.font(.subheadline)
}
HStack {
Text("Selezionato")
.font(.subheadline)
Spacer()
Text(selectedTopping ?? "")
}
Nota come ho modificato il set
del Binding
utilizzando il ternary operator. Quando l'utente seleziona il valore ""
passeremo allo @State
il valore nil
.
Conclusione
Il type Optional
da qui in avanti vedremo svolgono un ruolo fondamentale nella gestione di dati che potrebbero essere assenti o non ancora definiti.
Nella prossima lezione, dove vedremo come definire nuove strutture dati, ci saranno d'aiuto nella definizione di var
che non hanno un valore di partenza.
Al solito, se hai avuto qualche problema, lascia un commento sotto.
Buona programmazione!