If-else e switch in SwiftUI. Le istruzioni condizionali di Swift
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 nellaConsole
di Xcode. Potrai anche utilizzarla all'interno dei tuoi progetti per controllare velocemente i valori o funzionamento del tuo codice.Per il momento utilizzeremo
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 conif
infatti non potrai scrivere un'istruzioneelse
senza unif
.
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 dellavar
,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 delcase
verrà soddisfatta.La keyword
default
permette di definire un branch che cattura tutti i casi non specificati daicase
soprastanti.
A differenza di
if
dove le{}
definivano un branch. Perswitch
il branch è definito dalla keywordcase
.
La keyword
default
è l'equivalente dielse
per unif
.
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 ildefault
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 scrittocase let x
, questa è un'altra sintassi valida per poter scrivere istruzioni all'interno di uncase
.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 valoretrue
.
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 leggeand
, e viene utilizzato per eseguire un branch quando entrambe le istruzioni restituisconotrue
.||
, si leggeor
, e viene utilizzato per eseguire un branch quando una delle due istruzioni restituiscetrue
.
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 loswitch
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
quandox > 30
.cyan
quandox < 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 relativax
edy
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:
-
Uno
Stepper
(guida ufficiale qui) permette di incrementare/decrementare il numero di pizza margherita.Una margherita costa 4.5 euro.
-
Un
TextField
che permette di aggiungere un discount code.Il discount code è
PIZZA-10
e permette di ridurre il prezzo finale del 10%.
-
Una
Section
di riepilogo con i seguenti valori:Prezzo totale senza sconto.
Percentuale di sconto (visibile solamente se l'utente inserisce il codice valido)
Prezzo totale con sconto applicato (visibile solamente se l'utente inserisce il codice valido)
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!