Uno dei control flow più importanti in computer science è sicuramente il for loop. L'istruzione for permette di navigare una collezione o range di elementi e, per ogni iterazione, eseguire un blocco di codice.

Il ciclo for è l'istruzione che sta alla base di componenti grafici come List in SwiftUI o in metodi come filter e contains offerti dal type Array.

Capire come si crea un for loop e come funzionano è importante per due motivi principali:

  1. Alcune funzionalità dei tuoi futuri progetti potrebbero dipendere dal sapere iterare collezioni di elementi.
    Per esempio, il gioco Tic Tac Toe (il Tris) richiede saper iterare una matrice o Array di Array. A fine di questo modulo vedremo come realizzarlo.

  2. Se stai cercando lavoro, molti dei test riguardano risolvere problemi di iterazione senza utilizzare le funzionalità del framework Foundation.

Quindi, indipendentemente dalle motivazioni, apri il Playground e vediamo insieme come creare un ciclo for con il linguaggio Swift.

Come creare un ciclo for in Swift

Un loop per il linguaggio Swift può essere creato utilizzando le due keyword principali: for ed in:

for iterator in collection {

}

La keyword for sta ad indicare che stiamo creando un loop mentre la keyword in identifica la collezione o range che attraverseremo durante il ciclo for.

Per iterator si intende la costante che cattura, per ogni iterazione, i valori della collezione. Puoi usare qualsiasi nome come iterator ma, in linea di massima, si utilizza un nome singolare.

Il blocco di codice all'interno del ciclo for viene eseguito per ogni valore della collection ed i valori vengono attraversati da sinistra verso destra.

let numbers = [9, -10, 30, -45]

for number in numbers {
  print(number)
}

Nell'esempio, l'iterator chiamato number cattura i valori della collezione numbers e, per ogni ciclo, viene stampato il numero in console.

Facciamo un altro esempio ed immaginiamo d'avere Array di nomi dove, per ognuno di loro, vogliamo sapere da quanti caratteri è composto. Allora potremo utilizzare il for in così:

let names = ["Giuseppe", "Emily", "Luca", "Enzo"]

for name in names {
  print("\(name) has \(name.count) characters")
}

Complichiamo leggermente il nostro ciclo for ed ipotizziamo di voler mostrare un messaggio d'errore se l'array contiene un nome vuoto. Nessuno ci vieta di poter aggiungere un'istruzione if all'interno dei ciclo for:

let names = ["Giuseppe", "Emily", "", "Luca", "Enzo"]

for name in names {
    if name.isEmpty {
        print("Error: there is an empty element")
    } else {
        print("\(name) is \(name.count) characters long")
    }
}

Il componente ForEach di SwiftUI è l'equivalente di un ciclo for in.

Scope del ciclo for

Prima di passare a qualche esercizio, è importante capire che il codice definito all'interno del for viene resettato ogni qual volta l'iterator passa da un valore al successivo.

Immaginiamo di voler sommare tutti i valori contenuti all'interno di un Array, se definisco la var sum all'interno del ciclo for questa verrà distrutta e creata ad ogni iterazione, ottenendo un risultato inaspettato:

for number in [4, 6, 5] {
  var sum = 0
  sum = sum + number
  print(sum) // WRONG! sum = 5
}

Ovvero sum, venendo inizializzata con il valore 0 ad ogni iterazione, il ciclo finisce sommando sum + 5 (che è l'ultimo valore dell'array).

Per risolvere questo problema, la variabile sum deve essere inizializzata fuori:

var sum = 0

for number in [4, 6, 5] {
  sum = sum + number
}

print(sum) 

Enumerated for-in loop

In certe circostanze potrebbe capitare di voler catturare, insieme ad ogni elemento, anche la posizione (aka index) in cui si trova.
In questi casi, viene in aiuto il metodo degli Array enumerated() che restituisce una collezione speciale chiamata EnumeratedSequence dove ogni elemento viene trasformato in una coppia di valori: index e l'oggetto trasportato.

Per poter attraversare una EnumeratedSequence dovrai utilizzare un Iterator con due valori (index, object):

var names = ["Giuseppe", "Emily", "Luca", "Enzo"]

print(names)

for (index, name) in names.enumerated() {
  print("\(name) at index: \(index)")
}

// Giuseppe at index: 0
// Emily at index: 1
// Luca at index: 2
// Enzo at index: 3

Utilizzare sequenze enumerated ti permetterà, per esempio, di modificare l'array senza dover cercare l'indice dell'elemento iterato.

var names = ["Giuseppe", "Emily", "", "Luca", "Enzo"]

for (index, name) in names.enumerated() {
    if name.isEmpty {
        print("Error: there is an empty element at index: \(index)")
        names.remove(at: index)
    } else {
        print("\(name) is \(name.count) characters long")
    }
}

print(names)

Nell'esempio sopra ho utilizzato index per eliminare dall'array names gli oggetti con stringhe vuote. Senza utilizzare enumerated avrei dovuto cercare l'indice ed eventualmente accertarmi di star eliminando l'elemento corretto.

enumerated in SwiftUI

Nel caso in cui volessi utilizzare il metodo enumerated all'interno di List o ForEach, dovrai convertire la EnumeratedSequence in un nuovo Array.

ForEach(Array(names.enumerated()), id: \.element) { index, element in
  Text(element)
}

Nella lezione precedente, avremmo potuto utilizzare enumerated per semplificare il codice delle swipeActions:

ForEach(Array(todos.enumerated()), id: \.element) { index, todo in
    Text(todo)
        .swipeActions(edge: .leading) {
            Button("Complete") {
                todos.remove(at: index)
                completedCount += 1
            }
            .tint(.green)
        }
        .swipeActions(edge: .trailing) {
            Button("Delete", role: .destructive) {
                todos.remove(at: index)
                deletedCount += 1
            }
        }
}

Infatti, senza enumerated avremmo dovuto recuperare l'index utilizzando il metodo firstIndex(of:).

Convertire enumerated in Array è un'operazione obbligatoria perché sia List che ForEach possono interagire solamente con type che implementano il protocollo RandomAccessCollection.

Esercizio 0. Trova il valore max di un Array

Il ciclo for è probabilmente una delle istruzioni più complesse da comprendere, per questo motivo è importante esercitarsi fin dal primo istante si incontra questo nuovo strumento.

Il primo esercizio che devi provare a svolgere è: trova il numero più grande all'interno di un Array di Int.

var numbers = [5, -4, 55, 9, 100, 0, 22]

for number in numbers {
  // max = 
}

// print(max) - 100

Tips

Per poter risolvere questo problema ti serviranno due strumenti:

  1. Una var in cui conservare il valore max. Potresti, per esempio, ipotizzare che il primo valore dell'array è il più grande ed eventualmente sostituirlo non appena il ciclo for entra in funzione.

    1. Attenzione a dove definisci questa var, infatti aggiungerla all'interno del ciclo for potrebbe avere un comportamento inaspettato.

  2. Un if per poter confrontare il valore iterato dal ciclo con il valore considerato come max (ed eventualmente sostituirlo).

Al solito, scrivimi sotto nei commenti la tua soluzione o se hai avuto dei problemi.

Esercizio 1. Moltiplica gli elementi di due array

Immaginiamo d'avere due array points e factors con stessa dimensione (stesso numero di elementi):

  • Utilizza un ciclo for per moltiplicare i i valori allo stesso indice tra di loro.

  • Conserva i valori all'interno di un array results

let points: [Double] = [5, 10, 2, 4]
let factors: [Double] = [2, 3, 4, 1]

var results: [Double] = []

for (index, point) in points.enumerated() {
  // results
}

print(results) // [10, 30, 8, 4]

Tip: utilizza il metodo enumerated() per recuperare l'index. Utilizza l'index per leggere i valori dell'altro array.

Control flow: continue e break

Nel caso in cui volessi saltare un ciclo di iterazione o uscire dal ciclo puoi utilizzare rispettivamente le istruzioni continue e break.

Per esempio, immaginiamo di voler effettuare manualmente una ricerca all'interno di un array senza utilizzare il metodo contains. Potresti utilizzare break per evitare di iterare tutti gli elementi nel caso in cui il ciclo incontrasse l'elemento cercato prima della fine della collezione:

let names = ["Alice", "David", "Eve", "Bob", "Charlie"]

let searchName = "David"

for name in names {
    if name == searchName {
        print("\(searchName) found!")
        break
    }
    
    print("looking at: \(name)")
}

Senza il break il ciclo continuerebbe anche dopo aver incontrato l'elemento che stavamo cercando inizialmente.

In casi di array con migliaia o centinaia di miglia di elementi, utilizzare il break può migliorare drasticamente le performance delle tue app.

Dall'altro lato, l'istruzione continue ti permette di saltare al ciclo successivo evitando di eseguire il codice all'interno del for.

Come esempio, ipotizziamo di voler trasformare gli elementi di un array di Int da negativi in positivi, potremmo utilizzare il continue per skippare gli elementi che già sono positivi:

var numbers = [1, -2, -3, 4, 5, 6, -7, 8, 9, -10]

for (index, number) in numbers.enumerated() {
    if number > 0 {
      // the element is already positive, 
      // continue to the next cycle
      continue 
    }
    
    numbers[index] = -number
}

print(numbers)

Infatti, se togli l'istruzione continue e l'if puoi notare come tutti gli elementi vengono trasformati nel loro opposto.

Senza conoscere l'istruzione continue avresti dovuto innestare il codice all'interno di un if o else rendendo il codice leggermente più complesso.

Esercizio 2. Conta le vocali in una stringa

Utilizzando un ciclo for conta le vocali presenti all'interno di una stringa. Le stringhe sono speciali collezioni, di conseguenza possono essere iterate con un for in.

let text = "Swift is awesome!"
let vowels = ["a", "e", "i", "o", "u"]

var vowelCount = 0

for char in text.lowercased() {
   
}

Rappresentare Range di valori

Il linguaggio Swift offre un tipo di dato chiamato range che permette di rappresentare intervalli di valori. Per esempio, i numeri da 5 a 10 sono un intervallo di numeri Int e potranno essere rappresentati utilizzando il type Range o ClosedRange.

I Range a differenza degli Array vengono definiti esplicitando solamente l'estremo inferiore lowerBound e l'estremo superiore upperBound. Swift penserà a riempire i valori intermedi.

Una volta che hai identificato i due estremi, potrai creare:

  • ClosedRange, ovvero un range che contiene entrambi gli estremi, utilizzando la sintassi implicita lowerBound...upperBound.

  • Range o range aperto, ovvero che non contiene l'estremo superiore, utilizzando la sintassi lowerBound..<upperBound.

0...5 // ClosedRange 
0..<5 // Range
let fromZeroToFive: ClosedRange<Int> = 0...5
let fromZeroToFour: Range<Int> = 0..<4

Puoi apprezzare la differenza utilizzando il ciclo for per iterarne i valori. L'iterator catturerà ogni singolo valore presente all'interno dell'intervallo:

for n in 0...5 {
    print("n", n)
}

print("......")

for i in 0..<5 {
    print("i", i)
}

I range sono particolarmente utili, oltre che per eseguire cicli costanti, per catturare valori che rientrano all'interno del loro intervallo. Infatti, ipotizziamo un'app che permette la registrazione ad utenti di età compresa tra 18 e 99, potremo scrivere il nostro if o switch così:

let age = 30

if (18...99).contains(age) {
  print("user can signup")
}

switch age {
case 18...99: 
  print("user can signup")

default: break
}

Esercizio 3. Piramide di numeri

Utilizzando un ClosedRange ed un ciclo for stampa in console il seguente output:

1
1 2
1 2 3
1 2 3 4
1 2 3 4 5 
for i in 1...5 {
    var row = ""
        
    print(row)
}

Tips

In questo esercizio dovrai annidare un nuovo ciclo for all'interno del ciclo principale. In questo modo potrai, per ogni valore del ciclo, creare i numeri che ti serviranno per riempire la riga.

Invece di guardare al ciclo nella sua interezza, concentrati sui singoli step. Per esempio, l'iterazione numero 3, ovvero dove i ha valore 3, genererà la stringa 1 2 3. Come posso creare i numeri da 1 ad i?

Potresti utilizzare un ClosedRange specificando come upperBound il valore i:

1...i

Di conseguenza se isoliamo questo step in un esercizio a se, potremo creare la stringa 1 2 3 scrivendo il seguente codice:

let i = 3

var s = ""
for n in 1...i {
  s += "\(n) "
}

print(s) // 1 2 3

Se adesso porti questo concetto all'interno del ciclo principale, dovresti essere in grado di completare l'esercizio.

Conclusione

Anche se molto delle operazioni che richiederebbero eseguire un ciclo for sono già rappresentate da metodi come filter, firstIndex(of:), contains etc, a mio avviso è comunque importante capire come utilizzare questo strumento.

Infatti, oltre a capire meglio il dietro le quinte, ci permetterà di creare codice strettamente legato ai requirements delle nostre app e che non potrebbe altrimenti essere riprodotto utilizzando i metodi standard.

Per qualsiasi problema, al solito, scrivimi un commento qui sotto.

Buona programmazione!