Programmare in SwiftUI: introduzione agli stati
Come @State lega una view SwiftUI ai suoi dati, perché supera l'approccio dei controller UIKit e come estrarre subview e passare loro un modello.
Tradotto dall’inglese con l’aiuto dell’IA e rivisto dall’autore.Leggi l’originale

Abbiamo visto in un articolo precedente che in SwiftUI una View è una struct che adotta il protocol View. Quindi non eredita alcuna stored property, semplicemente perché non ha nessun padre; viene allocata sullo stack e gestita per valore e non per riferimento (come accade invece con le class in UIKit).
Come funzionano le view in SwiftUI
Dietro le quinte, SwiftUI gestisce le View con l'aiuto di una struttura dati molto efficiente per il rendering a schermo dei contenuti. Per questo motivo è consigliabile usare e creare componenti specializzati e riutilizzabili. Durante lo sviluppo, Cupertino ci rassicura, non dobbiamo esitare a estrarre le view che svolgono un compito specifico e a creare per loro un oggetto View dedicato.
Insieme al body, una View in SwiftUI definisce le sue dipendenze.
Supponiamo di voler creare in SwiftUI una View specializzata nel creare e gestire un pulsante. Il nostro oggetto dovrà occuparsi di un solo compito: alla pressione, la label dovrà cambiare riflettendo il nuovo stato.
Ed è proprio qui che la programmazione dichiarativa dà il meglio: la conseguenza di un'azione come modo per aggiornare l'interfaccia.
L'attributo @State in SwiftUI
Il nostro pulsante avrà quindi una dipendenza tra il suo stato e la sua rappresentazione a schermo. In SwiftUI questa dipendenza è gestita automaticamente dal nuovo modifier @State. Proviamo con un esempio per capire meglio il concetto.
struct MyButton : View {
@State private var toggleStatus:Bool = false
var body: some View {
Button(action: {
self.toggleStatus = !self.toggleStatus
}) {
Text((toggleStatus == false) ? "Press Me" : "Unpress me")
}
}
}
@State lega la variabile toggleStatus all'interfaccia: ogni volta che il suo valore cambia, SwiftUI sa che è necessario generare un nuovo body (e quindi aggiornare la label associata).

Provate a rimuovere @State dal codice precedente. Cosa succede e perché?
Rimuovendo l'attributo @State, il compilatore si lamenterà perché stiamo cercando di modificare un valore immutabile: non dimentichiamo che abbiamo a che fare con una struct. Ogni oggetto che viene renderizzato a schermo, infatti, è immutabile per ragioni di performance. In questo modo SwiftUI sa cosa deve essere aggiornato e cosa può essere lasciato fuori. Aggiungendo un modifier @State, stiamo dicendo esplicitamente a SwiftUI:
"Attenzione! Qui c'è un oggetto che potrebbe cambiare il suo stato, quindi dovrai aggiornarne il contenuto."
Di conseguenza, SwiftUI definisce implicitamente una dipendenza tra questa variabile e la presentazione a schermo della View ogni volta che incontra un attributo @State. In questo modo, ogni volta che la variabile cambia valore, SwiftUI sa che la View deve essere ridisegnata con il nuovo valore.
Per fissare questo concetto e vedere quanto sia diverso dalla programmazione tradizionale, facciamo un confronto con UIKit:
import UIKit
class ViewController: UIViewController {
@IBOulet var aButton: UIButton!
var toggleStatus:Bool = false
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func buttonDidPressed(sender: UIButton) {
sender.setTitle((toggleStatus) ? "Unpress me" : "Press me", for: .normal)
toggleStatus = !toggleStatus
}
}
Naturalmente in UIKit il codice qui sopra non basta per ottenere una schermata funzionante. Dovremo creare uno Storyboard con un UIViewController e un UIButton. Dovremo inoltre allineare il pulsante al centro con i Constraints e creare i riferimenti @IBOutlet e @IBAction corretti per far funzionare l'intero flusso.
Lasciando da parte la quantità di codice e di operazioni, ovviamente maggiore rispetto a SwiftUI, concentriamoci sul nostro stato. Anche in questo caso, infatti, il nostro pulsante deve conoscere lo stato corrente per poter cambiare la sua rappresentazione (la label). Notate come, nella programmazione imperativa tradizionale, ci siano due problemi concettuali importanti:
- toggleStatus è gestito (allocato, inizializzato e modificato) dal Controller.
- Non esiste un modo chiaro, a livello di codice, per separare le variabili che riguardano lo stato dell'interfaccia da quelle tradizionali: sono tutte uguali!

È anche evidente come la complessità di questo approccio cresca in modo esponenziale all'aumentare del numero di oggetti e di stati.

C'è ancora una cosa da sottolineare: iOS è un sistema operativo multithreaded. Questo significa che le richieste di aggiornamento dello stato e dell'interfaccia possono arrivare da più punti del codice e che i thread lavorano in modo indipendente l'uno dall'altro. Senza un modo chiaro per gestire situazioni di questo tipo si arriva a incoerenze nella UI e quindi a bug difficili da scoprire e da correggere.

La ricerca della verità
Ma come viene gestita in SwiftUI questa dipendenza tra variabili e interfaccia? E cosa rende il sistema resiliente e scalabile, a differenza di un approccio imperativo tradizionale?
Quando SwiftUI elabora una View con una variabile di tipo @State, alloca in memoria uno spazio per questa variabile per conto della View.

Aggiungendo l'attributo @State possiamo modificare un oggetto che sarebbe altrimenti immutabile, come una struct: dietro le quinte, infatti, la variabile viene allocata dal framework SwiftUI in un'area di lettura/scrittura dell'applicazione.
In SwiftUI ogni possibile stato di una View (l'offset di una scrollview, lo stato di un pulsante, il contenuto di uno stack e così via) è gestito da questo concetto, noto come Source of Truth. Seguendo questo concetto è possibile classificare ogni variabile come source of truth oppure come valore derivato.

Nel nostro esempio, toggleStatus è una source of truth. Inoltre, dallo schema qui sopra possiamo dedurre che anche tutte le costanti sono una source of truth, mentre una semplice property è un valore derivato.
Vedremo nei prossimi capitoli che esiste un altro meccanismo per passare valori derivati in lettura/scrittura, chiamato @Binding. Con un esempio pratico vedremo anche in cosa un valore derivato differisce dalla corrispondente source of truth. Analizzeremo le differenze con @State e come e quando usare l'uno rispetto all'altro.
Organizzare il codice: l'estrazione delle view
Ora che abbiamo chiaro come una View si aggiorna e ricostruisce il suo contenuto, proviamo a mettere insieme quello che abbiamo imparato con l'esempio dell'ultimo articolo. L'ultima volta avevamo lasciato la nostra ContentView così:
import SwiftUI
struct ContentView : View {
var body: some View {
VStack {
HStack {
Image("photoThumb")
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
.frame(width: 60, height: 60, alignment: .center)
.padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 0))
VStack(alignment: .leading) {
Text("Mario Rossi").font(.title)
Text("Amministratore delegato").font(.subheadline).fontWeight(.thin)
}
Spacer()
}
}
}
}
Chiaramente questo approccio non è mantenibile. Il codice tende ad annidarsi e a crescere verso destra, nella cosiddetta "pyramid of doom".
All'inizio dell'articolo abbiamo accennato a un meccanismo per estrarre le subview, così da rendere il codice scalabile e mantenibile. Vediamo cosa ci offrono il nuovo Xcode 11 e SwiftUI per risolvere questo problema.
Assicuratevi di usare Xcode 11 e portate il mouse sul tag HStack. Tenendo premuto il tasto CMD (⌘), cliccate sul tag, come segue:


Xcode crea automaticamente una nuova struct, estrae il contenuto selezionato e lo usa per popolare il body dell'oggetto appena generato. Alla fine dell'operazione ci offre anche la possibilità di cambiare il nome: rinominate la View estratta in CustomTableViewCell.
La nuova versione di ContentView è ora molto più leggibile e leggera, e contiene solo un riferimento a un nuovo oggetto. Come ulteriore comodità, potremmo anche spostare il contenuto di CustomTableViewCell in un nuovo file, per isolare meglio il concetto di componente.
Passare parametri
Non sarebbe interessante passare i parametri giusti alla nostra nuova CustomTableViewCell, per generare una cella con contenuti dinamici come titolo, sottotitolo e immagine? Proviamo a modificare la nostra nuova struct per ottenere questo risultato:
struct CustomTableCell : View {
var photoName:String
var title:String
var subtitle:String
var body: some View {
return HStack {
Image(photoName)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
.frame(width: 60, height: 60, alignment: .center)
.padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 0))
VStack(alignment: .leading) {
Text(title).font(.title)
Text(subtitle).font(.subheadline).fontWeight(.thin)
}
Spacer()
}
}
}
Ora, dentro la ContentView, la nostra CustomTableCell può essere creata con parametri che ne definiscono in modo univoco il contenuto.
struct ContentView : View {
var body: some View {
VStack {
CustomTableCell(photoName: "photoThumb",
title: "Gianluca Verdi",
subtitle: "Consigliere")
}
}
}
E se volessimo spingerci oltre e creare, per esempio, un pulsante che cambia il contenuto del campo subtitle?
Il modello, l'oggetto e SwiftUI
Il modello collegato a un oggetto View ha un ruolo fondamentale nel paradigma dichiarativo di SwiftUI. Ogni oggetto ha tipicamente un modello associato che, insieme allo stato, identifica completamente l'interfaccia utente mostrata.
Per esempio, potremmo associare alla nostra CustomTableViewCell un modello di questo tipo:
struct Person {
var photoName:String
var title:String
var subtitle:String
}
Di conseguenza dovremo modificare la nostra View per sfruttare il nuovo modello appena creato:
struct CustomTableViewCell : View {
var model: Person
var body: some View {
return HStack {
Image(model.photoName)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
.frame(width: 60, height: 60, alignment: .center)
.padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 0))
VStack(alignment: .leading) {
Text(model.title).font(.title)
Text(model.subtitle).font(.subheadline).fontWeight(.thin)
}
Spacer()
}
}
}
Infine, la ContentView deve riflettere questa modifica per mostrare correttamente a schermo la nostra CustomTableViewCell.
struct ContentView : View {
@State private var model = Person(photoName: "photoThumb",
title: "Mario Rossi",
subtitle: "Amministratore delegato")
var body: some View {
VStack {
CustomTableViewCell(model: model)
Button(action: {
self.model.subtitle = "Consigliere"
}) {
Text("Change subtitle")
.foregroundColor(Color.white)
.padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8))
.background(Color.blue)
.cornerRadius(8)
}
}
}
}
Questa volta la variabile model ha l'attributo @State: il proprietario del modello è l'oggetto ContentView, che lo passa per copia (ricordiamo che stiamo lavorando con struct e non con class) alla CustomTableViewCell, la quale a sua volta lo usa per mostrare i campi corrispondenti dei suoi oggetti: Image e Text.
Il pulsante ha un'azione che aggiorna il modello cambiandone il subtitle. Poiché ContentView ha aggiunto l'attributo @State al suo modello, ogni modifica di quest'ultimo fa scattare l'aggiornamento del suo body che, a sua volta, rigenera una nuova CustomTableViewCell con il contenuto aggiornato.

Avrete notato che ho aggiunto l'attributo private prima di dichiarare la variabile di stato del modello. Questo perché, come abbiamo spiegato sopra, le variabili marcate con @State appartengono esclusivamente alla View che le dichiara. È quindi una buona idea rafforzare questo concetto marcandole come private.
Vedremo nel prossimo articolo che SwiftUI espone altri modi per gestire i parametri e l'injection quando ci troviamo in situazioni come quella illustrata sopra. In questo caso, infatti, pur essendo corretto dal punto di vista sintattico e funzionale, il nostro flusso soffre di un piccolo problema: la nostra CustomTableViewCell usa una propria copia del modello, che a sua volta è esposto, allocato e gestito da ContentView con una variabile privata.
SwiftUI offre astrazioni molto simili all'attributo @State per gestire queste circostanze e permettere al modello di propagarsi correttamente dove serve.
Cosa succede se cambiamo il modello da struct a class?
Anche se il progetto continuerà a compilare e a girare senza problemi, purtroppo noteremo che in pratica il pulsante non cambierà più il contenuto del campo subtitle.
@State funziona con i value type, mentre le class sono reference type: cambiando il valore di una class marcata con l'attributo @State, la sua modifica interna non fa scattare l'aggiornamento del body.
-- 🖖🏻
