State e Binding in SwiftUI
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 variabileBool
. Questo perché in SwiftUI le string interpolation devono essere eseguite tutte su oggetti di typeString
.
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)
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 unBinding<URL>
..dateTime
che accetta unBinding<Date>
. In questo caso, però, consiglio di utilizzare il componenteDatePicker
(sotto trovi un esempio)..number
oltre che aDouble
lo potrai utilizzare perInt
.
Ricordati di utilizzare il modifier
.keyboardType
più vicino alformat
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!