Attivare o meno una porzione di codice in base a delle condizioni è ciò che permetterà di dar vita alle tue applicazioni.

Tutti i linguaggi di programmazione, infatti, offrono delle istruzioni che permettono di modificare il flusso di codice. Queste prendono il nome di istruzioni condizionali.

Ti faccio un esempio.

Immagina di start sviluppando la funzionalità Checkout di un'app di Booking. Questa UI ha un TextField che permette di inserire un discount code. Se l'utente inserisce un codice valido, uno sconto del 10% viene applicato all'ordine.

Le istruzioni condizionali ti permetteranno di catturare questo tipo di logiche e funzionamento. Tutto ciò che puoi tradurre in "se questo succede allora" potrà essere riprodotto il codice utilizzando istruzioni condizionali.

Per il linguaggio Swift, quattro sono le keyword principali che potrai utilizzare per scrivere conditional logic: if-else, guard e switch.

In questa lezione studieremo prima le istruzioni all'interno del Playground e poi vedremo come queste si comportano quando utilizzate in SwiftUI.

Let's start!

Il concetto di condizione

Per capire le istruzioni condizionali, dobbiamo prima soffermarci sul concetto di condizione.

Per il linguaggio Swift una condizione è un'operazione che restituisce un oggetto Bool. Ho spiegato cos'è un boolean in questa lezione.

Per esempio, se scrivo a == b, dove a e b sono due variabili, agli occhi del linguaggio Swift questa operazione restituisce un Bool che sarà true o false in base al valore delle var.

Capito che alla base di un'istruzione condizionale c'è un'operazione booleana, vediamo la prima istruzione: if ed else.

if e if-else in Swift

La prima istruzione è if. Questa ci permette di eseguire un codice quando una condizione è true. La sintassi richiede:

  • prima la keyword if

  • poi un'istruzione che genera un Bool

  • seguito da un blocco di parentesi graffe { }

  • All'interno dell graffe inserirai il codice che verrà attivato quando la condizione è true:

if booleanCondition {
  // true
}

Facciamo un primo semplice esempio.

Immaginiamo la classica applicazione bancaria che controlla il valore di withdraw, se il valore è maggiore del balance vogliamo informare l'utente con un errore:

var balance: Double = 100.0

var error: String = ""

let withdraw: Double = 200

if withdraw > balance {
  // show an error to the user
  error = "You can't withdraw an amount greater than your balance"
}

print(error)

Dato che withdraw è maggiore di balance Swift entrerà ad eseguire il codice all'interno delle graffe ed, nel nostro caso, la variabile error verrà cambiata di valore.

Nell'esempio ho scritto print(error) quest'istruzione permette di scrivere nella Console di Xcode. Potrai anche utilizzarla all'interno dei tuoi progetti per controllare velocemente i valori o funzionamento del tuo codice.

Per il momento utilizzeremo print come sistema di Debug del nostro codice. Vedremo più in là che esistono modi migliori per debuggare il codice.

Se if permette di attivare codice quando una condizione è true, allora else ci permette di attivare un codice quando questa è false.
La sintassi richiede la keyword else subito dopo l'ultima parentesi graffa dell'if:

let withdraw: Double = 50

if withdraw > balance {
  // show an error to the user
  error = "You can't withdraw an amount greater than your balance"
} else {
  balance = balance - withdraw
  error = ""
  print("your new balance is:", balance)
}

Cambiando il valore di withdraw ad uno < di balance vedremo che Swift eseguirà il codice dentro l'else. Nel nostro esempio abbiamo eseguito l'aggiornamento del balance ed eseguito un print del nuovo bilancio.

else va sempre in combinazione con if infatti non potrai scrivere un'istruzione else senza un if.

Se alcune delle nozioni in questa lezione ti suonano nuove, dai un'occhiata alle lezioni precedenti del corso SwiftUI Tour.

Operatore ternario

Spesso capiterà di voler generare un valore sul momento in base al risultato di un'operazione condizionale.

In Swift, ma anche in altri linguaggi, esiste un'operatore chiamato ternary-operator che permette di calcolare un valore su singola linea utilizzando un'operazione condizionale. La sintassi è la seguente:

operazione ? valueForTrue : valueForFalse
let number = 2
var isTwo = number == 2 ? "is two" : "is not two"

Per esempio, se volessimo sapere se un'operazione bancaria è un withdraw o deposit potremmo controllare se il valore è negativo o positivo:

let operationValue = -100
var operationType = operationValue < 0 ? "withdraw" : "deposit"

Vedremo che questa sintassi ci tornerà molto utile in SwiftUI quando ci servirà aggiornare il valore di modifiers senza necessariamente ricreare la view (in fondo trovi degli esempi).

Branch e internal branch

Le parentesi graffe, in un contesto di operazione condizionale vengono chiamate branch. Un branch è una porzione di codice che può o no essere eseguita durante il funzionamento dell'app.

Nel caso di if-else l'istruzione ha due branch uno che può essere attivato dall'if ed uno dall'else.

Un branch può avere branch al suo interno, per esempio, nessuno ti vieta di aggiungere un nuovo if all'interno di uno dei suoi branch.

if withdraw > balance { // 1 branch
  if withdraw > 9_999 { // 2 branch
    print("Are you an hacker?")
  }
}

if-else-if, if all'ennesima potenza

Spesso capiterà di voler controllare diverse condizioni in cascata. Sempre dall'esempio sopra, immaginiamo di voler informare l'utente che sta per prelevare tutto il suo balance quando i due valori combaciano.

Quindi avremo:

  • withdraw > balance: non puoi eseguire l'operazione.

  • withdraw == balance: sei sicuro di voler eseguire l'operazione?

  • withdraw < balance: puoi eseguire l'operazione.

Possiamo combinare un'altra istruzione if quando il branch else viene attivato:

if withdraw > balance {

} else if withdraw == balance {

} else {

}

Nota come non ho scritto l'ultimo else come else if withdraw < balance perché è implicito che quel branch catturerà quei valori.

In tutta onestà if-else-if non è una sintassi molto utilizzata nel linguaggio Swift. Infatti, in casi di multi branch è consigliato utilizzare l'operatore switch.

Vediamo come utilizzare switch in Swift.

Switch in Swift

L'operatore switch, a differenza di if che richiede operazioni Bool, permette di catturare un qualsiasi valore e confrontarlo contro diverse condizioni o case.

Un case per uno switch rappresenta una operazione condizionale a cui il valore catturato verrà confrontato:

let number = 0

switch number {
case 0:
    print("It's 0")

case 1:
    print("It's 1")

case 100:
    print("It's 100")

default:
    print("It's something else.")
}

La sintassi è la seguente:

  • switch seguito dal nome della var, let o valore da confrontare.

  • Parentesi graffe {}.

  • All'interno delle graffe inserirai i case.

  • Un case viene seguito dal valore da confrontare ed i due punti :

  • All'interno dei : scriverai il codice da eseguire quando la condizione del case verrà soddisfatta.

  • La keyword default permette di definire un branch che cattura tutti i casi non specificati dai case soprastanti.

A differenza di if dove le {} definivano un branch. Per switch il branch è definito dalla keyword case.

La keyword default è l'equivalente di else per un if.

I case catturano valori in base al type passato allo switch. Infatti se passi un oggetto String i valori che dovrai scrivere all'interno del case saranno di type String.

Esempio

Immaginiamo d'avere un'app che in base al livello di membership, definisce un tipo di sconto da applicare al checkout.
Ipotizziamo le seguenti membership: gold, silver, bronze che rispettivamente garantiscono il 15, 10 e 5 di sconto.

Grazie ad uno switch potremo associare membership a discount in modo più snello rispetto a scrivere la stessa istruzione in if-else:

var membership = "gold"
var discount: Double = 0

switch membership {
case "gold":
    discount = 15
    
case "silver":
    discount = 10
    
case "bronze":
    discount = 5
    
default:
    discount = 0
}

print("your discount is: \(discount)%")

Il default va sempre inserito quando la variabile catturata può avare qualsiasi valore. Se rimuovi il default Xcode ti segnalerà un errore.

Come catturare più valori in un case

Infine, nel caso in cui volessi catturare più valori ed eseguire lo stesso codice, potrai dividere questi da una virgola:

var planet: String = "mars"

switch planet {
case "mars", "venus", "earth": 
  print(planet, "is a planet")

case "pluto", "moon":
  print(planet, "is not a planet")
}

Eseguire operazioni condizionali in un case

L'istruzione switch per il linguaggio Swift permette di definire case che contengono istruzioni condizionali. La sintassi da utilizzare nel case è la seguente:

var number = 5

switch number {
case number where number > 5: 
case number where number < 5:
case number where number < 0:
default:
}

Ovvero prima si cattura la variabile e successivamente si aggiunge la keyword where seguita dall'operazione condizionale sulla variabile catturata.

Faccio un esempio più complesso, ed immaginiamo di voler calcolare la generazione dei nostri utenti in base al loro year di nascita. Potremmo usare questa sintassi così:

let year = 1993

switch year {
case let x where x > 1997 && x > 2012:
    print("you are Gen Z, year", x)
    
case let x where x > 1981 && x < 1996:
    print("you are Millennials, year:", x)
    
default:
    break
}
  • Nell'esempio invece di scrivere case year where year ho scritto case let x, questa è un'altra sintassi valida per poter scrivere istruzioni all'interno di un case.

    • Puoi anche usare la x all'interno del branch.

  • Nota come ho utilizzato l'operatore && per combinare più operazioni condizionali. Vedremo tra un attimo questo simbolo permette di combinare condizioni in modo tale che il codice venga eseguito quando entrambe generano un valore true.

L'istruzione break

In certi casi capiterà di voler uscire da un case e dall'istruzione switch al verificarsi di certe condizioni.

L'istruzione di control-flow chiamata break, come la parola suggerisce, interrompe l'esecuzione dello switch e riprende l'esecuzione subito dopo le sue parentesi graffe.

var isLoggedIn = false

switch isLoggedIn {
case true:
  var balance = 100
  let withdraw = 300

  if withdraw > balance { 
    print("Disallowed, you can't withdraw more than your balance.")
    break // exit from switch
  }

  balance = balance - withdraw
  print("successfully withdrawn, your balance is now:", balance)

case false: break
}

Grazie a break quando withdraw è > balance potremo evitare di eseguire un'operazione di prelievo che manderebbe in negativo il nostro conto.

Combinare istruzioni condizionali con && o ||

Nel caso in cui volessi eseguire più controlli su un valore potrai utilizzare:

  • &&, si legge and, e viene utilizzato per eseguire un branch quando entrambe le istruzioni restituiscono true.

  • || , si legge or, e viene utilizzato per eseguire un branch quando una delle due istruzioni restituisce true.

Ti scrivo degli esempi e lascio al te il compito di capire quali dei seguenti branch viene eseguito:

let number: Int = // scrivi qui il valore per eseguire l'if sotto

if number == 0 || number < -10 {
  print(number)
} 

let membership: String = // scrivi qui il valore

switch membership {
case let x where x == "gold" && x == "silver":
  print(membership)
  break

default:
 break
}

Adesso che abbiamo imparato ad utilizzare l'istruzione if e switch con il linguaggio Swift, non ci resta altro che applicare questi concetti a SwiftUI.

Operazioni condizionali in SwiftUI

Grazie agli operatori condizionali if e switch potremo modificare le nostre UI in base al valore degli @State della nostra View.

Anche se utilizzare if ed switch vedremo non richiede molta differenza rispetto a scrivere del codice Swift, dobbiamo fare attenzione ad alcune regole di SwiftUI che potranno impattare negativamente l'esperienza delle tue UI.

Ma, prima di arrivare a parlare di View Hierarchy ed Identity, vediamo alcuni esempi di if e switch in SwiftUI.

Nascondere e mostrare view utilizzando if in SwiftUI

Immaginiamo d'avere un Form dove un Button compare solamente quando l'utente accetta i termini e condizioni.

In primis, potremo usare il componente Toggle per dare all'utente la possibilità di accettare o meno i T&C e, successivamente, utilizzare un if per mostrare il Button quando lo @State hasAcceptedTerms è uguale a true:

@State var hasAcceptedTerms: Bool = false 

var body: some View {
    Form {
        Toggle("Do you accept our T&C?", isOn: $hasAcceptedTerms)
        
        if hasAcceptedTerms {
            Button("Continue") {
                
            }
        }
    }
}

SwiftUI tiene d'occhio tutti gli @State presenti all'interno della nostra view e quando un if o else può essere soddisfatto ne renderizza il suo branch.

Nota come SwiftUI ha animato l'aggiunta del Button o la sua rimozione. In base al contesto SwiftUI decide quale animazione meglio soddisfa l'aggiunta o rimozione di una view.

EmptyView e switch

Nel caso in cui avessi bisogno di gestire multi branch con una switch e non volessi mostrare nessuna View, ricordati che in SwiftUI non puoi utilizzare l'istruzione break.

In questi casi puoi utilizzare il componente EmptyView per simboleggiare l'assenza di view.

Per esempio, immaginiamo di voler mostrare un tag accanto al nome utente. Questo tag cambia in base al livello di membership ed è assente quando l'utente non possiede una membership valida:

let membership: String = "gold"

switch membership {
case "gold": Text("gold")
case "silver": Text("silver")
default: EmptyView()
}

Attenzione all'ordine e livello delle istruzioni

Adesso che comincerai a combinare logiche condizionali ad UI devi entrare nell'ottica di testare tutti i casi possibili in modo tale da evitare di introdurre bugs più o meno gravi.

Nell'esempio sotto, la VStack contiene uno switch che restituisce una EmptyView di default. Il problema qui è che la VStack ha dei modifiers che le permettono d'avere un background con dei bordi arrotondati. Di conseguenza, quando lo switch è EmptyView ci ritroveremo con un tag senza testo:

struct ContentView: View {
    
    let membership: String = "test"
    let username: String = "peppe.app"
    
    var body: some View {
        Form {
            Text(username)
                .font(.title3)
            
            VStack {
                switch membership {
                case "gold": Text("gold")
                case "silver": Text("silver")
                default: EmptyView()
                }
            }
            .fontWeight(.bold)
            .foregroundStyle(.orange)
            .padding(.vertical, 8)
            .padding(.horizontal, 12)
            .background(Color.orange.opacity(0.1))
            .clipShape(.rect(cornerRadius: 6))
            
        }
    }
}

Come risolviamo questi problemi?

Una soluzione potrebbe essere quella di utilizzare un if per mostrare o meno l'intera VStack solamente quando membership è gold o silver. Nell'esempio sopra scriveremo:

if membership == "gold" || membership == "silver" {
    VStack {
        Text(membership)
    }
    .fontWeight(.bold)
    .foregroundStyle(.orange)
    .padding(.vertical, 8)
    .padding(.horizontal, 12)
    .background(Color.orange.opacity(0.1))
    .clipShape(.rect(cornerRadius: 6))
}

Adesso l'intera VStack è gestita dall'if, di conseguenza verrà rimossa o aggiunta alla View qualora la condizione dell'if è soddisfatta. Nota anche come ho rimosso lo switch all'interno della VStack, infatti quello non era strettamente necessario in quanto sappiamo che una volta entrati dentro il branch, il valore di membership sarà solamente gold oppure silver.

Puoi ottenere lo stesso risultato utilizzando uno switch, prova come esercizio ed eventualmente scrivimi un commento sotto. L'unico tip è che lo switch deve avere questa struttura, lascio a te il compito di completarlo:

switch membership {
case "gold", "silver": 

default:

}

Creare nuove View vs aggiornarne il contenuto

Ogni if o switch che SwiftUI esegue ha come effetto l'aggiunta o rimozione della view o views all'interno dei loro branch.

Spesso però capiterà di voler semplicemente aggiornare una View in base al risultato di un'operazione condizionale.

Per esempio, ipotizziamo d'avere un Rectangle che possiamo muovere a destra e sinistra premendo dei Button. Quando il rettangolo supera un certo threshold cambierà di colore.

  • orange quando x > 30.

  • cyan quando x < 30.

  • black in tutti gli altri casi.

Con le nozioni che abbiamo accumulato fin ora, ti potresti ritrovare a scrivere un codice dove ad ogni branch il Rectangle viene ricreato (ricordati che scrivere Type e parentesi tonde significa invocare il suo costruttore):

VStack {
    if x > 30 {
        Rectangle()
            .offset(x: x)
            .foregroundStyle(.orange)
            .frame(width: 60, height: 60)
    } else if x < -30 {
        Rectangle()
            .offset(x: x)
            .foregroundStyle(.cyan)
            .frame(width: 60, height: 60)
    } else {
        Rectangle()
            .offset(x: x)
            .foregroundStyle(.black)
            .frame(width: 60, height: 60)
    }
}

Il modifiers offset permette di modificare la posizione relativa x ed y della view. Qui la documentazione ufficiale.

Queste tipologie di codice potrebbero risultare in glitch grafici oltre che a complicare inutilmente la sintassi della tua app.

Come risolviamo?

Possiamo utilizzare l'operatore ternario, spiegato all'inizio della lezione, per cambiare sul momento il valore di foregroundStyle:

Rectangle()
    .offset(x: x)
    .foregroundStyle(x > 30 ? .orange : .cyan)
    .frame(width: 60, height: 60)

In questo modo non servirà più scrivere quella catena di if-else o switch e SwiftUI utilizzerà solamente un oggetto Rectangle però cambiando il suo valore di foregroundStyle quando x cambia.

Nel codice però c'è un problema. Riesci ad individuarlo?

Infatti utilizzando il ternary operator abbiamo involontariamente eliminato la condizione in cui il Rectangle debba essere black in tutti gli altri casi.

Tecnicamente potresti risolvere scrivendo un nuovo operatore ternario nella componente else ovvero:

.foregroundStyle(x > 30 ? .orange : (x < -30 ? .cyan : .black))

Questa sintassi è sconsigliata in quanto complessa da seguire.

Allora come possiamo fare?

Utilizzare computed property per calcolare valori complessi

Quando hai a che fare con calcolare valori per le tue view o modifiers che richiedono complessi if-else o switch è consigliato spostare quella logica all'interno di una computed property.

Nell'esempio sopra, potremmo creare una computed property chiamata rectangleStyle, in cui calcolare il colore da utilizzare, per poi passarla al modifiers foregroundStyle:

// fuori dal body
var rectangleStyle: Color {
    switch x {
    case x where x > 30: .orange
    case x where x < -30: .cyan
    default: .black
    }
}

// nel body 
.foregroundStyle(rectangleStyle)

Di seguito il codice completo:

import SwiftUI

struct ContentView: View {
    
    @State var x: Double = 0
    
    var rectangleStyle: Color {
        switch x {
        case x where x > 30: .orange
        case x where x < -30: .cyan
        default: .black
        }
    }
    
    var body: some View {
        VStack {
            VStack {
                Rectangle()
                    .offset(x: x)
                    .foregroundStyle(rectangleStyle)
                    .frame(width: 60, height: 60)
            }
            .frame(height: 400)
            
            Spacer()
            
            Form {
                Section {
                    Button("Move Right") {
                        x = x + 10
                    }
                    
                    Button("Move Left") {
                        x = x - 10
                    }
                }
            }
        }
    }
}

Esercizio 1. Trova quale dei due numeri è il più grande?

Crea un Form con due TextField di type Int. In base al valore del campo A e B dovrai mostrare il seguente testo:

  • A is greater than B

  • A is less than B

  • A is the same as B

Esercizio 2. Checkout con Discount code

Immaginiamo d'avere una pizzeria che serve solamente margherita e di voler creare una pagina di checkout che permette di aumentare/diminuire il numero di pizze ed un field che permette di inserire un discount code.

In particolare dovrai creare una UI con i seguenti requirements:

  1. Uno Stepper (guida ufficiale qui) permette di incrementare/decrementare il numero di pizza margherita.

    1. Una margherita costa 4.5 euro.

  2. Un TextField che permette di aggiungere un discount code.

    1. Il discount code è PIZZA-10 e permette di ridurre il prezzo finale del 10%.

  3. Una Section di riepilogo con i seguenti valori:

    1. Prezzo totale senza sconto.

    2. Percentuale di sconto (visibile solamente se l'utente inserisce il codice valido)

    3. Prezzo totale con sconto applicato (visibile solamente se l'utente inserisce il codice valido)

  4. Un Button che viene attivato/disattivato se il prezzo totale è > 0.

In pratica, la tua UI dovrà assomigliare a qualcosa di simile a questo:

Nei commenti trovi alcuni tips su come realizzarla.

Conclusione

Grazie alle istruzioni condizionali come if e switch finalmente le tue applicazioni potranno cominciare a prendere vita. Adesso che abbiamo questi strumenti potremo cominciare ad esplorare componenti come NavigationView, List e elementi del linguaggio Swift come Array, Dictionary e cicli for e funzioni.

Ad ogni modo, se hai avuto qualche problema, al solito scrivi un commento qui sotto.

Buona programmazione!