Programmare in SwiftUI: ObjectBinding e BindableObject
Come condividere un modello basato su class tra view SwiftUI con @ObjectBinding e BindableObject, notificando i cambi con il PassthroughSubject di Combine.
Tradotto dall’inglese con l’aiuto dell’IA e rivisto dall’autore.Leggi l’originale

In un articolo precedente abbiamo visto che in SwiftUI è possibile associare uno o più stati a una View tramite il modifier @State; quest'ultimo deve essere usato con value type allocati all'interno della View che li dichiara. In questo modo la nostra applicazione è in grado di capire autonomamente quando aggiornare l'interfaccia utente in seguito ai cambiamenti della variabile marcata con @State che, ricordiamo, viene identificata come source of truth.
Ma cosa succede se la nostra source of truth è un oggetto complesso, progettato per esempio come una class? In questo caso @State fallisce il suo scopo, perché è pensato per lavorare esclusivamente con le property locali della nostra View.
SwiftUI mette a disposizione un concetto più adatto a questo caso: @ObjectBinding. Si può usare in tutti i casi in cui la property viene passata per riferimento, come accade con le class.
struct MyCustomObject : View {
@ObjectBinding var myObject
var body: some View {
Text(myObject.someTextProperty)
}
}
Il protocol BindableObject
Le property con un modifier @ObjectBinding si riferiscono quindi a oggetti di tipo class. Perché una class sia compatibile con il modifier @ObjectBinding, deve adottare il protocol BindableObject.
Nella sua variante più semplice il protocol BindableObject è molto lineare e richiede soltanto che la nostra class implementi una property chiamata didChange, che notificherà gli oggetti View iscritti in modo che possano reagire correttamente ai cambiamenti dell'interfaccia visiva.
class MyObject : BindableObject {
let didChange = PassthroughSubject<Void, Never>()
var someTextProperty:String = "" {
didSet {
didChange.send( () )
}
}
...
}
Il framework Combine e PassthroughSubject
Perché funzioni, il codice precedente richiede di includere il supporto per il framework Combine all'inizio del nostro file swift.
import Combine
Il framework Combine è uno strumento molto potente e piuttosto complesso, che vedremo in un prossimo articolo. Per ora ci basta sapere che, tra le tante altre cose, fornisce una class chiamata PassthroughSubject (la nostra property didChange è un'istanza di questa class). Ha il compito di notificare (Publishing) le parti interessate (Subscribers) che si è verificato un evento specifico.
PassthroughSubject, come suggerisce il nome, si comporta in modo molto simile alle notifiche tradizionali messe a disposizione dal framework Foundation di Swift con NSNotificationCenter: l'oggetto specificato come parametro (nel nostro caso Void, dato che non vogliamo passare alcun parametro) viene passato così com'è, senza modifiche, al destinatario insieme a un evento di notifica che fa scattare una nuova ricostruzione della view.
Se si vuole specificare un errore insieme alla notifica, lo si può passare come secondo parametro. Nel nostro esempio questa eventualità non ci interessa, ed è per questo che passiamo Never come secondo parametro nell'invocazione di PassthroughSubject.
let didChange = PassthroughSubject<Void, Never>()
Una volta creata un'istanza di tipo PassthroughSubject, la possiamo usare per pubblicare il nostro evento ogni volta che vogliamo notificare alle View iscritte i cambiamenti della nostra source of truth.
Per soddisfare quest'ultimo requisito usiamo il costrutto didSet messo a disposizione da Swift, che viene invocato ogni volta che cambia il valore di una property:
var someTextProperty:String = "" {
didSet {
didChange.send( () )
}
}
In questo modo, quando modifichiamo someTextProperty, viene invocato il costrutto didSet. Al suo interno chiamiamo il metodo send() dell'istanza di PassthroughSubject. Notate come specifichiamo un parametro di tipo Void passando al metodo una coppia di parentesi ().
Così tutti gli oggetti iscritti ai cambiamenti (quelli che hanno dichiarato la loro property come @ObjectBinding) sapranno che è arrivato il momento di aggiornare il loro body.

Come potete vedere dallo schema qui sopra, il nostro modello è caratterizzato da una singola istanza in memoria, tramite una class conforme al protocol BindableObject. I nostri oggetti View contengono un riferimento all'oggetto originale.
In questo modo si evita un problema tipico, la duplicazione della source of truth: le modifiche al modello saranno uniche e porteranno all'aggiornamento immediato di tutte le view che fanno riferimento alla nostra class.

Un esempio
Proviamo a mettere in pratica i concetti precedenti con un esempio. Nell'articolo precedente avevamo configurato la nostra CustomTableViewCell per usare una variabile passata per valore, contenente una struct chiamata Person.
struct CustomTableViewCell : View {
var model: Person
var body: some View {
...
Questo approccio non permetteva alla CustomTableViewCell di essere indipendente rispetto agli aggiornamenti del proprio contenuto: la variabile, infatti, veniva copiata durante la fase di init della CustomTableViewCell.
Ogni modifica al suo contenuto era gestita tramite una variabile @State, posseduta da ContentView, che rigenerava la cella con il nuovo contenuto a ogni cambiamento.

Con il concetto di @ObjectBinding possiamo rendere unico il modello Person, passando alla CustomTableViewCell solo un riferimento, come mostrato nel grafico qui sopra a destra.
La prima cosa da fare è modificare il nostro modello Person in modo che usi una class che adotta il protocol BindableObject al posto di una struct.
class Person : BindableObject {
let didChange = PassthroughSubject<Void, Never>()
var photoName:String { didSet { update() } }
var title:String { didSet { update() } }
var subtitle:String { didSet { update() } }
init(photoName: String, title: String, subtitle: String) {
self.photoName = photoName
self.title = title
self.subtitle = subtitle
}
func update() {
didChange.send(())
}
}
Il nostro nuovo modello adotta il protocol BindableObject implementando la property didChange tramite un'istanza di PassthroughSubject.
Ogni property interessata da possibili cambiamenti dell'interfaccia visiva (photoName, title e subtitle) è stata modificata in modo da reagire tramite il costrutto didSet. In questo modo, ogni volta che aggiorniamo una delle variabili, viene attivata la funzione update(), che a sua volta invia una notifica a tutti gli iscritti tramite l'istanza di PassthroughSubject.
Infine modifichiamo la CustomTableViewCell per sfruttare il nuovo modello per riferimento. Per farlo basta anteporre il modifier @ObjectBinding di cui abbiamo parlato.
struct CustomTableCell : View {
@ObjectBinding 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)
VStack(alignment: .leading) {
Text(model.title).font(.title)
Text(model.subtitle).font(.subheadline).fontWeight(.thin)
}
Spacer()
}
}
}
In questo modo la CustomTableViewCell non ha una copia del modello Person, ma lavora con un riferimento all'area di memoria in cui è conservato il modello originale. Vediamo dove e come creare il modello da passare per riferimento.
Di questo requisito si occuperà ContentView, creando il modello vero e proprio in memoria e passandolo per riferimento alla CustomTableViewCell.
struct ContentView : View {
var dataSource:[Person] = [
Person(photoName: "photoThumb", title: "Mario Rossi", subtitle: "Amministratore"),
Person(photoName: "photoThumb", title: "Giuseppe Bianchi", subtitle: "Consigliere"),
Person(photoName: "photoThumb", title: "Luca Verdi", subtitle: "Funzionario"),
]
var body: some View {
List {
CustomTableCell(model: dataSource[0])
CustomTableCell(model: dataSource[1])
CustomTableCell(model: dataSource[2])
}
}
}
List e TableView
Questa nuova versione di ContentView usa un nuovo oggetto chiamato List, che raggruppa gli elementi più o meno come il corrispettivo VStack. La differenza sta nella visualizzazione: il risultato sarà molto simile a una UITableView del framework UIKit.
Quella che vediamo qui sopra è la forma di dichiarazione più semplice di List. Nel prossimo articolo vedremo come rendere dinamico il suo contenuto, in base al modello dichiarato.
--A presto!
