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 print che stampa in console "Hello from" seguito dal nome della 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 o let (nell'esempio result) 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 una label chiamata where 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 premi Return 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 di VStack, per esempio, scrivila come VStack { } invece di VStack(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!