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() e Int.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() restituisce true o nil se false.

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 dalla func.

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!