Funzioni e Closure in Swift
Se c'è una skill che uno sviluppatore deve assolutamente possedere è quella di saper dividere un problema in tanti piccoli sotto problemi più facili da gestire e risolvere. Questa pratica prende il nome latino di divide et impera.
Se stai seguendo il corso SwiftUI Tour molto probabilmente è perché hai in mente di sviluppare un'applicazione. Qualsiasi sia la tua idea, ti rederai presto conto di come questa sia più complessa di quanto potessi immaginare.
Non importa quale sia il tuo livello di programmazione, qualsiasi progetto risulterà complesso ed irraggiungibile se guardato nella sua interezza.
Quindi, come si affronta lo sviluppo di un'applicazione?
La soluzione, come spesso accade, ce la da la storia ed in particolare i nostri vecchi e cari romani.
Divide et impera o divide and conquer è un paradigma la quale idea di base è quella di decomporre un problema in due o più semplici sotto problemi. Risolvere i sotto problemi di conseguenza porterà al completamento del problema originale.
Nel caso in cui i sotto problemi siano ancora complessi, questi possono essere decomposti ulteriormente.
Questa procedura di continuare a scomporre un problema viene spesso chiamata ricorsione o Recursion.
Ho spiegato cos'è un paradigma nella lezione in cui abbiamo parlato di Object Oriented Programming.
Immagina di voler ricreare l'app di Instagram. Vista per intero può sembrare un progetto mastodontico, ed ovviamente lo è, ma applicando il paradigma divide et impera possiamo almeno cominciare a svilupparla partendo da semplici problemi. Per esempio, potremo scomporre il lavoro nei seguenti tasks:
-
Come posso scattare foto e video?
Che framework utilizzare?
Come eseguo l'upload di foto/video su un server?
Come creo una lista di immagini che l'utente può scrollare verticalmente?
Come controllo l'autenticazione utente?
etc etc
Capito il principio, adesso vediamo come possiamo applicarlo in Swift e SwiftUI.
Nel linguaggio Swift una delle pratiche che ci permette di applicare il paradigma divide et impera è data dalla possibilità di creare funzioni.
Cos'é una funzione in Swift?
Una funzione è un blocco di codice che permette eseguire un determinato task. Una func
ha un nome che ne identifica l'operazione e che puoi utilizzare per eseguire il task quando necessario.
Per esempio, il modifier font di SwiftUI, è una funzione che dietro il suo nome esegue un codice che modifica il font del Text
su cui viene applicato. Grazie al fatto che la complessità di cambiare font è incapsulata (nascosta) dietro la funzione permette a noi sviluppatori di preoccuparci solamente del loro utilizzo e risultato.
Quindi, in questa lezione vedremo come creare nuove funzioni e come utilizzarle per scomporre la logica delle tue app in componenti riutilizzabili e facili da comprendere.
Let's start!
Come definire una funzione senza parametri in Swift
Le funzioni in Swift vengono definite utilizzando la keyword func
. Questa va seguita dal nome della funzione, un set di parantesi tonde ed una di graffe.
In questa semplice configurazione, la funzione non accetta parametri in ingresso e non restituisce valori in uscita:
func someFunctionName() {
print("Hello from", #function)
}
In questo esempio la funzione invoca la funzione
La sintassi
#function
permette di catturare il nome della funzione.
Per utilizzare una funzione ti basterà scrivere il suo nome seguito dalle parentesi tonde all'interno del file o scope in cui è stata definita:
someFunctionName() // first invocation
someFunctionName() // second invocation
Se ti ricordi dalla lezione sui Type ed oggetti, abbiamo scoperto come i Type offrono delle funzionalità che ti permettono di interagire con i loro oggetti.
I modifiers per esempio sono delle funzioni che accettano o meno dei parametri in ingresso o producono dei valori in uscita.
Un esempio di funzione che non produce o accetta valori in ingresso è toggle
ed è offerto dal type Bool
. Il metodo toggle
permette di settare il valore al suo opposto: ovvero da true
a false
o viceversa. Un po' come premere un interruttore:
var isOn: Bool = false
print("isOn:", isOn) // false
isOn.toggle()
print("isOn:", isOn) // true
Un altro esempio di funzione senza parametri è negate()
del type Int
. Questa converte il valore nel suo opposto negativo:
var number = 1
print("number", number) // 1
number.negate()
print("number", number) // -1
Funzioni con parametri in ingresso
I parametri in ingresso di una funzione vengono chiamati argomenti. Un argomento, dietro le quinte, è una costante che cattura il valore passato al momento dell'utilizzo della funzione.
Creare un argomento richiede un nome ed il type. Nel caso volessi aggiungerne più di uno, ti basterà separarli da una virgola:
var playerX = 0
var playerY = 0
func movePlayer(x: Int, y: Int) {
print("moving player to x: \(x), y: \(y)")
playerX = x
playerY = y
}
movePlayer(x: 10, y: 5)
movePlayer(x: -4, y: 3)
Nell'esempio la funzione movePlayer
ha due argomenti x
ed y
di tipo Int
. Entrambi vengono utilizzati per settare le variabili playerX
e playerY
.
Label
Gli argomenti possono avere una label, o nome accessorio, che ti permetterà di rendere più espressivo l'utilizzo delle tue funzioni. Puoi aggiungere una label prima del nome del parametro:
func send(money value: Double) {
print("sending \(value)")
}
send(money: 200)
Quando aggiungi una label, questa viene utilizzata al momento dell'utilizzo della funzione. Il parametro, invece, verrà utilizzato solamente all'interno del corpo della funzione.
Puoi aggiungere label a tutti i parametri di una funzione.
Nel caso in cui volessi omettere la label al momento dell'utilizzo, puoi utilizzare il simbolo _
:
func title(_ content: String) {
print(content)
}
title("hello, world!")
Parametri in uscita
Una funzione può essere utilizzata, oltre per incapsulare codice, per produrre nuovi valori o risultati. Possiamo definire parametri in uscita, aggiungendo alla fine della definizione dei parametri, la sintassi ->
seguita dal type che otterremo utilizzando la funzione:
func sum(a: Int, b: Int) -> Int {
a + b
}
let result = sum(a: 2, b: 3)
print(result) // 5
Nell'esempio la funzione sum
accetta due parametri a
e b
e restituisce la somma tra i due.
Come puoi notare, i parametri d'uscita non hanno label. Questo è voluto in quanto una la funzione verrà generalmente assegnata ad una
var
olet
(nell'esempioresult
) o passata come parametro di un altra funzione.
Quando una func
ha un type d'uscita, e più di un'istruzione, dovrai obbligatoriamente utilizzare la keyword return
per restituire un valore all'esterno.
func sum(name: String, surname: String) -> String {
if name.isEmpty || surname.isEmpty {
return ""
}
return "\(name) \(surname)"
}
Scope, body e dependecy di una funzione
Il codice che scrivi all'interno delle parentesi graffe della definizione di una funzione prende il nome di body. Dall'interno del body potrai accedere alle variabili e costanti definite all'esterno, mentre il viceversa sarà proibito.
let a = 5
let b = 6
func test() -> Int {
// body della funzione
let sum = a + b // OK. you can access external var
return sum
}
print(sum) // ERROR! Can't access sum.
Esercizio 0. Discount Calculator
Crea una funzione che accetta due parametri price
(un Double
) e discount
(Int
che rappresenta la percentuale) e restituisce il prezzo con lo sconto applicato:
let priceA = applyDiscount(percentage: 10, to: 100) // 90
let priceB = applyDiscount(percentage: 5, to: 100) // 95
Esercizio 1. Calcola la media di un Array
Crea una funzione che accetta un array di Int
e restituisce la media Double
dei valori:
let averageA = average([10, 15, 30, 7, 2]) // 12.8
let averageB = average([4, 6, 9]) // 6.3
Esercizio 2. Check vocali in una stringa
Crea una funzione che restituisce true
se tutti i caratteri in una String
sono vocali:
let stringA = isVowels(string: "ae") // true
let stringB = isVowels(string: "abcde") // false
Tip. Esercizio 1 e 2 richiedono l'utilizzo di un ciclo for all'interno della funzione.
Il type di una funzione
Per il linguaggio Swift anche le funzioni hanno un Type che li definisce e loro sintassi dipende dai parametri in ingresso ed uscita.
Prima di vedere la sintassi, devo introdurti il concetto di type Void
. Questo type speciale rappresenta l'assenza di type ed i suoi oggetti vengono creati utilizzando la sintassi ()
:
let voidObject: Void = ()
Tutte le funzioni che non restituiscono valori o che non posseggono parametri ingresso hanno type Void
. Per esempio, la seguente funzione title(value: String)
:
func title(value: String) {
return () // can be omitted
// or return
}
è equivalente alla seguente:
func title(value: String) -> Void {
return () // can be omitted
// same as just return
}
Il linguaggio Swift, quando una funzione ha type Void
in uscita, aggiunge in maniera automatica l'istruzione return ()
. Di conseguenza, quando scrivi una funzione di questo tipo puoi tranquillamente omettere sia il -> Void
che il return ()
.
Capito il type Void
, diremo che la funzione title(value: String)
ha type: (String) -> ()
. Facciamo qualche altro esempio:
func doSomething() {} // () -> ()
func sum(name: String, age: Int) -> String // (String, Int) -> String
func isLoggedIn() -> Bool // () -> Bool
Nel caso di funzioni con più parametri in ingresso, i loro type vengono divisi da virgola e racchiusi tra parentesi tonde.
Avendo le funzioni dei type, nessuno ci vieta di assegnare una funzione ad una variabile o costante. Per farlo ti basta passare il nome di una funzione ad una let
o var
:
func signInUser(email: String, password: String) {
print("login with email: \(email)")
if password == "1234" {
print("login success")
}
}
let login = signInUser
login("test@email.com", "1234")
Nell'esempio la costante login
viene inizializzata con signInUser
che essendo una funzione trasforma la costante in una sorta di trigger. Infatti, al suo utilizzo la costante login
ci permette di passare i due parametri email
e password
tra parentesi tonde.
Che cosa posso farci con questa proprietà delle funzioni?
In alcuni casi, passare una funzione ti permetterà di semplificare e spezzare le tue logiche. Dato che le funzioni hanno un type, se prendi in esempio il Button
di SwiftUI, adesso possiamo comprendere quel famoso parametro action
e label
:
let button = Button(action: () -> Void, label: () -> View)
Uno degli init
del type Button
richiede come action
una funzione () -> Void
e per la label
una funzione () -> View
.
Di conseguenza potresti definire delle funzioni con quella stessa sintassi e passarne il riferimento:
func doSomething() {
print(#function)
}
Button(action: doSomething) {
Text("Hello")
}
Cos'è una Closure e come utilizzarle
Con le conoscenze che abbiamo appreso fin ora, finalmente possiamo comprendere una delle sintassi più complesse del linguaggio Swift: le closure.
Ma, non preoccuparti, senza saperlo hai già utilizzato questo strumento più di una volta quindi ci basterà solamente codificarlo in termini tecnici in modo tale da applicarlo con cognizione di causa.
Una closure è un blocco di codice, simile ad una funzione, che può essere passato come riferimento a parametri che accettano funzioni.
Se prendiamo come esempio il metodo filter
di un Array
notiamo come il suo parametro vuole il riferimento ad una funzione di type: (Int) -> Bool
, ovvero accetta un Int
in ingresso e deve restituire un Bool
in uscita:
La keyword
throws
è uno strumento di Swift che permette di generare e catturare errori da una funzione. Vedremo a cosa serve in una lezione dedicata.
Di conseguenza, potremmo definire una funzione e passare il suo nome come parametro del metodo filter
. Per esempio, ipotizziamo di voler filtrare tutti i numeri positivi:
func filterPositive(_ number: Int) -> Bool {
number > 0
}
let numbers = [1, -5, 2, 3]
numbers.filter(filterPositive(_:))
Se noti la funzione filterPositive
ha un type (Int) -> Bool
che è esattamente quello richiesto dal metodo filter
. Di conseguenza possiamo passare il suo nome come parametro.
Passare una funzione ad un parametro richiede scrivere il suo nome e le
label
dei parametri seguiti da:
. Non è richiesto scrivere type in uscita o i type dei parametri.
Grazie alle closure possiamo saltare questa definizione di funzione esterne ed, invece, possiamo scrivere il comportamento di parametri che accettano funzioni durante il loro utilizzo.
La sintassi per definire una closure in Swift è:
{ parameters in
// do something
}
Una closure può catturare i valori passati ai parametri della funzione utilizzando la sintassi parameter in
dove parameter
è una costante il quale nome può essere modificato da te in base al contesto.
Se una funzione non accetta parametri in ingresso, puoi omettere questa sintassi.
Nell'esempio del filter
dei numeri positivi, dato che il parametro della funzione è (Int) -> Bool
scriveremo la nostra closure come:
{ number in
number > 0
}
Ovvero:
let numbers = [1, -5, 2, 3]
numbers.filter { number in
number > 0
}
Nota come ho eliminato le parentesi tonde
()
dal metodo filter. Swift ci permette di snellire la sintassi per renderla più leggibile. Infatti, l'esempio sopra è equivalente a:numbers.filter({ number in number > 0 })
Facciamo un altro esempio, ed utilizziamo il metodo firstIndex(where:
per cercare l'indice da un array di nomi:
let names = ["peppe", "luca", "enzo"]
names.firstIndex { name in
name == "luca"
}
// same as
names.firstIndex(where: { name in
name == "luca"
})
firstIndex
ha unalabel
chiamatawhere
che può essere omessa quando utilizzi una closure come parametro. Il mio consiglio è di ometterle quando possibile, in genere Xcode ti aiuta in questo processo quando premiReturn
durante l'auto completamento della sintassi.
Catturare valori in maniera anonima
Nel caso in cui volessi scrivere le tue closure senza dover necessariamente definire i nome dei valori catturati utilizzando la sintassi { param1 in }
, potrai utilizzare una sintassi più snella che ti permette di catturare quei parametri in maniera automatica utilizzando la sintassi $0
, $1
etc (Il numero rappresenta l'ordine dei parametri passati alla closure).
Per esempio, potremo riscrivere gli esempi visti sopra così:
let numbers = [1, -5, 2, 3]
numbers.filter { $0 > 0 }
// --- //
let names = ["peppe", "luca", "enzo"]
names.firstIndex { $0 == "luca" }
In linea di massima, nel linguaggio Swift si consiglia di utilizzare questa sintassi nel caso in cui definire il nome dei parametri non porta ad un aumento di comprensione del codice.
Negli esempi sappiamo perfettamente che in numbers.filter { $0 > 0 }
il parametro anonimo $0
rappresenta ogni number
dell'array numbers
.
Closure in SwiftUI
A questo punto, hai tutte le conoscenze per comprendere la maggior parte delle sintassi viste in SwiftUI.
Per esempio, se guardiamo la VStack
, il suo parametro content
è una funzione di type () -> View
:
VStack(content: () -> View)
// Esempio
VStack {
Text("hello")
}
VStack(content: {
Text("hello")
})
Oppure, nel caso di Section
se analizziamo il costruttore content
e header
vediamo che entrambi accettano due funzioni:
Section(content: () -> View, header: () -> View)
// Esempio
Section {
Text("content")
} header: {
Text("Header")
}
Il suggerimento è quello di:
Utilizzare le closure il più possibile dato che rendono il codice più facile da seguire e comprendere.
Quando possibile, evita di scrivere i nomi di
label
a meno che non suggerito da Xcode. Nel caso diVStack
, per esempio, scrivila comeVStack { }
invece diVStack(content: {})
.
Conclusione
Dicevamo all'inizio di questo tutorial come le funzioni sono uno degli strumenti che ci permettono di dividere il nostro codice in piccoli blocchi riutilizzabili. Oltre ad avere una finalità organizzativa, c'è a mio avviso, un aspetto ancora più importante: leggibilità ed astrazione.
Se riesci a definire dei nomi di funzioni che comunicano il loro intento ed il loro risultato, ti ritroverai ad avere un codice espressivo e che, in una veloce lettura, trasmette il suo comportamento.
Per esempio, guarda il codice qui sotto e dimmi se riesci a capire cosa sta succedendo:
let driver = map.closestDriver(to: userPosition)
let response = driver.book(at: time, paymentMethod: .applePay)
if response.result == .booked {
showConfirmationScreen(response.details)
}
Anche senza conoscere l'applicazione ed il suo funzionamento, leggendo questo snippet di codice possiamo capire come un oggetto map
permette di recuperare il cosestDriver
in base alla userPosition
e di eseguire un booking book
utilizzando .applePay
come pagamento.
Da qui in avanti, utilizzando funzioni e tra qualche lezione vedremo le struct
, poniti come obiettivo quello di scomporre le tue logiche in codice che possa essere facilmente interpretabile da una singola lettura.
Buona programmazione!