Nella lezione precedente grazie all'attributo @State associato ad una variabile, SwiftUI riesce a ricalcolare tutte le view che utilizzano quel valore ogni qual volta quest'ultimo viene aggiornato.

Oggi vedremo che @State, oltre ad essere un sistema di refresh, permette la catturare di user input. Quest'operazione, in SwiftUI, prende il nome di Binding.

Un Binding permette ad una View di SwiftUI di modificare e leggere il valore di uno @State in maniera bidirezionale. Bidirezionale è la chiave per comprendere il Binding. Infatti, interagire con una View modificherà il valore dello @State ed, al tempo stesso, modificare lo @State aggiornerà la View associata.

Ma allora qual'è la differenza tra @State e Binding?

Il Binding è semplicemente una "connessione" tra uno @State ed una View che ne modificherà il contenuto. Mentre uno @State è confinato alla View che lo definisce, il Binding potrà essere trasportato giù nella gerarchia dei componenti e, di conseguenza, un sub-componente potrà modificare la view principale.

Ma, prima di arrivare a parlare di propagazione di State, partiamo da come trasformare uno State in un Binding utilizzando i componenti di SwiftUI: TextField, DatePicker, Toggle.

Bonus content, vedremo anche nuovi componenti per organizzare il nostro layout come Form e Section.

👋 I contenuti del video sono un po' diversi rispetto alla lezione scritta e contengono nozioni affrontate nella lezione sulle Variabili e State in SwiftUI. L'esercizio e codice del progetto sono in fondo alla lezione.

Let's start!

TextField & Toggle

Il componente TextField viene utilizzato per catturare user input su singola linea. Classico esempio è la TextField usata per creare pagine di login.

Nella sua forma più semplice, un TextField viene creato passando una label che rappresenta il placeholder ed un Binding. Vediamo il costruttore:

TextField(_ titleKey: LocalizedStringKey, text: Binding<String>)

Binding è un type particolare che fa parte della famiglia dei Generics. Le parentesi angolari stanno ad indicare che il Binding può accettare un oggetto del type definito nelle sue parentesi.

Nel caso della TextField in parametro text è un Binding di type String. Un altro esempio, se scrivo Binding<Double> sto definendo un type Binding che accetta un type Double.

Che valore devo passare nel campo text?

La logica ci dice che potremmo inizializzare un oggetto di type Binding utilizzando uno dei suoi costruttori, però, nel caso di @State abbiamo una sintassi che ci agevola nella trasformazione di State in Binding.

Per prima cosa, creiamo una @State var, chiamata email, in cui sincronizzeremo il valore della TextField.

Per trasformare una variabile di type State in Binding ti basterà aggiungere il simbolo $ davanti al nome della variabile:

@State private var email: String = ""
TextField("Email", text: $email)

Per rimarcare ancora il concetto, se ho una @State var signupNewsletter: Bool = false , potrò creare un Binding<Bool> scrivendo il simbolo dollaro $ davanti signupNewsletter:

@State var signupNewsletter: Bool = false
Toggle("Join Newsletter", isOn: $signupNewsletter)

Nota come nella string interpolation di "Are you subscribed" ho utilizzato il parametro description sulla variabile Bool. Questo perché in SwiftUI le string interpolation devono essere eseguite tutte su oggetti di type String.

Così com'è la TextField è abbastanza blanda, per fortuna potremo personalizzare il suo style utilizzando il modifiers textFieldStyle che accetta uno dei valori di default forniti dal type TextFieldStyle:

.textFieldStyle(_ style: TextFieldStyle)

// .automatic
// .plain
// .roundedBorder

Di default viene utilizzato .automatic infatti noterai da qui in avanti che TextField cambierà di style in base al contenitore in cui viene inserita.

Proviamo a passare .roundedBorder ed ecco qui il risultato:

TextField("Email", text: $email)
  .textFieldStyle(.roundedBorder)
textFieldStyle(.roundedBorder) in swiftUI

textContentType

Un altro modifiers che spesso assocerai ad una TextField è quello che ti permette di definire il contenuto e di conseguenza attivare i suggerimenti automatici da keyboard.

Questo modifier si chiama textContentType ed accetta un oggetto di type UITextContentType. Per esempio, nel caso del campo email passeremo il valore .emailAddress.

TextField("Email", text: $email)
  .textFieldStyle(.roundedBorder)
  .textContentType(.emailAddress)

Un altro modifiers utile è keyboardType che ti permette di cambiare la keyboard quando l'utente seleziona il campo. Per esempio .keyboardType(.decimalPad) sarà utile per campo di testo dove ti aspetti solamente input di type numerico (sotto un esempio).

Lascio al te il compito di analizzare gli altri modifiers e valori del TextField. Nella documentazione ufficiale trovi tanti esempi utili.

TextField e format

In alcuni casi particolari ti ritroverai a richiedere input di tipo numero. Per esempio, se stai chiedendo ad un utente di inserire la somma da prelevare dal conto in banca, quel valore inserito nel TextField sarà sicuramente un numero.

Per evitare di dover eseguire conversioni da String a Double, TextField offre la possibilità di accettare un Binding<Double> aggiungendo, al costruttore, il parametro format e passando come valore .number.

In questo modo, il tuo @State potrà essere definito come var number: Double. Esempio:

@State var depositAmount: Double = 0
TextField("Width", value: $depositAmount, format: .number)
  .keyboardType(.decimalPad)

Il vantaggio di usare format è quello di automatizzare la conversione e permetterti di usare @State diversi da String.

Altri format utili saranno:

  • .url dove potrai utilizzare un Binding<URL>.

  • .dateTime che accetta un Binding<Date>. In questo caso, però, consiglio di utilizzare il componente DatePicker (sotto trovi un esempio).

  • .number oltre che a Double lo potrai utilizzare per Int.

Ricordati di utilizzare il modifier .keyboardType più vicino al format che i tuoi utenti useranno.

SecureField

Parente stretto del TextField è il componente SecureField. Questo lo utilizzerai per input con valori nascosti dal classico *.

Lascio a te come esercizio il compito di provare il SecureField:

SecureField("Password", text: $password)

DatePicker

Una volta capito come funziona un Binding il gioco è sempre lo stesso. Per esempio, con il componente DatePicker potrai richiedere input di type Date.

I costruttori più utilizzati sono:

DatePicker(_ titleKey: LocalizedStringKey, selection: Binding<Date>)
DatePicker(_ titleKey: LocalizedStringKey, selection: Binding<Date>, displayedComponents: DatePickerComponents)

Un esempio, potrai utilizzare questo componente per richiedere la data d'invio di una notifica o la data di compleanno:

@State var date: Date = Date.now
DatePicker("Birthday", selection: $date)

Di default il DatePicker permette di selezionare sia date che time. Nel caso in cui volessi raccogliere solamente una parte della data, puoi inizializzare il DatePicker specificando il parametro displayedComponents.

Il displayedComponents vuole un oggetto di un type DatePickerComponents. Questo può essere inizializzato o passando un singolo valore di default, per esempio .date o passando più valori utilizzando la sintassi degli Array, ovvero [.time, .hourAndMinute]. Quest'ultima sintassi la studieremo nel dettaglio in una lezione dedicata.

Per esempio, se volessimo accettare solo orario come input, allora inizializzeremo il DatePicker così:

DatePicker("Birthday", selection: $date, displayedComponents: .hourAndMinute)

Fai attenzione!

Pur specificando il displayedComponents come hourAndMinute questo non significa che eliminerà la data dalla variabile associata. Infatti, hourAndMinute cambia solamente hour and minute:

DatePicker ha altre funzionalità come poter bloccare il Range delle possibili date selezionabili. Questa richiede una sintassi leggermente più complessa ma non impossibile, nella documentazione ufficiale trovi un esempio.

Nel caso dovessi incontrare dei problemi, al solito scrivimi un commento sotto.

Form e Section

TextField ed in generale input views trovano la loro massima espressione se usati con il componente Form. La view Form permette di raggruppare controlli ed input propagando uno style di default.

Infatti, in base alla piattaforma in cui eseguirai la tua app, Form applicherà lo stile di sistema. Per esempio, in iOS, Form dispone i componenti automaticamente in una List verticale.

Vediamo come usarlo. Il costruttore principale non accetta argomenti e si comporta come una VStack:

Form {

}

Una volta aggiunti dei componenti al suoi interno, vedrai che verranno raggruppati in una box e la view principale prenderà un background di default:

Form {
    TextField("Email", text: $email)
        .textContentType(.emailAddress)
    
    DatePicker("Birthday", selection: $date, displayedComponents: .hourAndMinute)
}

Nel caso in cui volessi raggruppare componenti in sezioni puoi utilizzare l'omonimo componente Section.

Form {
    Section {
        TextField("Email", text: $email)
            .textContentType(.emailAddress)
        DatePicker("Birthday", selection: $date, displayedComponents: .hourAndMinute)
    }
    
    Section {
        Toggle("Accept T&C", isOn: $hasAcceptedTerms)
    }
}

Section offre anche la possibilità di aggiungere un Header e Footer. Per farlo ti basterà aggiungere la closure header e/o footer:

Section {
  Text("Content")
} header: {
  Text("Header")
} footer: {
  Text("Footer")
}

Esercizio 1. Input e ridimensionamento Image.

Crea un semplice Form con due TextField di tipo numerico che permettono di modificare la width e height di una Image.

Suggerimenti:

  • Ricordati che puoi passare una variabile all'interno di una View e/o ad un parametro di un modifiers.

  • Il modifiers per modificare le dimensioni, lo abbiamo incontrato nella lezione sul componente Image, e si chiama frame.

Nel caso in cui dovessi avere dei problemi, lascia un commento in fondo alla lezione.

Codice progetto video

import SwiftUI

struct ContentView: View {
    
    @State
    var balance: Decimal = 1500.00
    
    @State
    var showBalance: Bool = true
    
    @State
    var receiverName: String = ""
    
    @State
    var transferAmount: Decimal = 0
    
    @State
    var transferDate: Date = .now
    
    let currencyCode = "AUD"
    
    var transferDetails: String { // get-only - computed property
        let amount = transferAmount.formatted(.currency(code: currencyCode))
        let date = transferDate.formatted(date: .abbreviated, time: .omitted)
        
        return "You are sending \(amount) to \(receiverName) on \(date)"
    }
    
    var body: some View {
        Form {
            overviewSection
            transferSection
            confirmSection
        }
    }
    
    var overviewSection: some View {
        Section {
            HStack {
                Text("Balance")
                Spacer()
                Text(balance, format: .currency(code: currencyCode))
                    .redacted(reason: showBalance ? .privacy : .placeholder)
            }
            
            Toggle("Show", isOn: $showBalance)
        } header: {
            Text("Overview")
        }
    }
    
    var transferSection: some View {
        Section {
            TextField("Name", text: $receiverName)
            TextField("Amount", value: $transferAmount, format: .number)
                .keyboardType(.decimalPad)
            DatePicker("Sending date", selection: $transferDate, displayedComponents: .date)
        } header: {
            Text("Transfer details")
        }
    }
    
    var confirmSection: some View {
        Section {
            
            if !receiverName.isEmpty {
                Text(transferDetails)
            }
            
            Button("Confirm transfer") {
                balance = balance - transferAmount
                
                transferAmount = 0
                transferDate = .now
                receiverName = ""
            }
            .disabled(transferAmount <= 0)
            
        } header: {
            Text("Confirm transfer")
        } 
    }
}

#Preview {
    ContentView()
}

Soluzione esercizio video

import SwiftUI

struct Exercise: View {
    
    @State var height: Double = 100
    @State var width: Double = 100
    @State var rotation: Double = 90
    @State var color: Color = .black
    
    var body: some View {
        Form {
            Section {
                HStack {
                    Text("Height")
                    Spacer()
                    TextField("", value: $height, format: .number)
                        .multilineTextAlignment(.trailing)
                        .frame(width: 100)
                }
                
                HStack {
                    Text("Width")
                    Spacer()
                    TextField("", value: $width, format: .number)
                        .multilineTextAlignment(.trailing)
                        .frame(width: 100)
                }
                
                HStack {
                    Text("Rotation")
                    Spacer()
                    TextField("", value: $rotation, format: .number)
                        .multilineTextAlignment(.trailing)
                        .frame(width: 100)
                }
                
                ColorPicker("Color", selection: $color)
            } header: {
                Text("Editor")
            }
            
        }
        VStack {
            Spacer()
            
            Rectangle()
                .foregroundStyle(color)
                .frame(width: width, height: height)
                .rotationEffect(.degrees(rotation))
        }
    }
}

#Preview {
    Exercise()
}

Conclusione

Grazie a State e Binding finalmente siamo riusciti a creare delle prime UI interattive.

Binding vedremo tra qualche lezione, ci permetterà di trasportare State da una view all'altra permettendoci così di modificare lo stato delle nostre UI da qualsiasi componente.

Ma prima di arrivare lì, dobbiamo aggiungere alla nostra cassetta degli attrezzi un altro argomento essenziale del linguaggio Swift: I conditional statement.

Buona programmazione!