ObjectBinding e BindableObject

Il concetto di stato esteso alle classi in SwiftUI

Negli articoli precedenti abbiamo visto che in SwiftUI è possibile associare uno o più stati ad una View attraverso il modificatore 
@State
; quest'ultimo va usato con tipi valore ed allocati all'interno della stessa View. In questo modo la nostra applicazione è in grado di capire autonomamente quando aggiornare l'interfaccia utente rispetto ai cambiamenti apportati alla nostra variabile marcata con @State che, ricordiamo, viene indentificata come sorgente di verità.

Ma cosa succede se la nostra sorgente di verità è un oggetto complesso realizzato attraverso l'uso del costrutto class? In questo caso @State fallisce nel suo intento perchè è pensato per lavorare con proprietà locali della View per valore.

SwiftUI mette a disposizione un ulteriore concetto per dialogare con l'eventualità in cui la nostra sorgente di verità sia una variabile complessa passata per riferimento, denominato @ObjectBinding.


struct MyCustomObject : View {
    @ObjectBinding var myObject
    var body: some View {
        Text(myObject.someTextProperty)
    }
}

Il protocollo BindableObject

Le proprietà con associato un modificatore @ObjectBinding si riferiscono, dunque, ad oggetti di tipo class. Per fare in modo che una classe sia compatibile con il modificatore @ObjectBinding è necessario adottare il protocollo BindableObject.

Il protocollo BindableObject è abbastanza semplice nella sua variante più comune e richiede unicamente che la nostra classe implementi una proprietà denominata didChange, la quale notificherà gli oggetti di tipo View sottoscritti così che essi possano reagire correttamente ai cambi visuali di interfaccia.


class MyObject : BindableObject {
    let didChange = PassthroughSubject<Void, Never>()
    
    var someTextProperty:String = "" {
        didSet {
            didChange.send( () )
        }
    }
    
    ...
}

Il framework Combine e PassthroughSubject

Per poter funzionare il codice sopra è necessario includere il supporto al framework Combine all'inizio del nostro file swift.

import Combine

Il framework Combine è uno strumento molto potente e per certi versi complesso che vedremo in un prossimo articolo. Per ora ci basta sapere che esso - tra le tante cose - mette a disposizione una classe denominata PassthroughSubject (la nostra property didChange è un'istanza di questa classe) attraverso cui è possibile notificare (Publishing) gli interessati (Subscribers) che un determinato evento è accaduto.

PassthroughSubject, come dice il nome stesso, si comporta in maniera molto simile alle notifiche tradizionali messe a disposizione dal framework Foundation di Swift con NSNotificationCenter: l'oggetto specificato come parametro (nel nostro caso Void, non vogliamo passare nessun parametro) viene passato in versione integrale e senza alcuna modifica al ricevente.

Nel caso si voglia specificare un errore nella gestione dell'evento di pubblicazione è possibile passarlo come secondo parametro: nel nostro esempio non siamo interessati a questa evenienza ed è il motivo per cui passiamo Never come secondo parametro.

let didChange = PassthroughSubject<Void, Never>()

Una volta creata un'istanza di tipo PassthroughSubject è necessario utilizzarla per effettuare il publishing del nostro evento ogni qualvolta desidereremo notificare le View interessate che la nostra sorgente di verità è cambiata.

Per fare questo usiamo il costrutto didSet messo a disposizione da Swift che viene invocato ogni qualvolta il valore di una proprietà cambia:

var someTextProperty:String = "" {
    didSet {
        didChange.send( () )
    }
}

In questo modo, quando effettueremo una modifica a someTextProperty il costrutto didSet verrà invocato ed al suo interno effettueremo una chiamata al metodo send( ) dell'istanza di PassthroughSubject. Nota come specifichiamo un parametro di tipo Void, passando una coppia di parentesi () all'interno del metodo.

Così facendo, tutti gli oggetti sottoscritti alle modifiche della nostra classe, ovvero quelli che hanno dichiarato una loro property come @ObjectBinding, sapranno che è arrivato il momento di rinfrescare il proprio body.

ObjectBinding Simple Flow State

Fig.1 - Schema allocazione e referenza oggetti con ObjectBinding.

Come si può vedere dallo schema sopra, il nostro modello è caratterizzato da un'unica istanza in memoria, attraverso una classe conforme al protocollo BindableObject. I nostri oggetti View contengono una referenza all'oggetto originale. In questo modo, si evita un caratteristico problema noto come duplicazione della sorgente di verità.

Le modifiche al modello saranno univoche e caratterizzate da un'immediato aggiornamento di tutte le view che referenziano la nostra classe.

SchemaSorgenteVeritaObjectBinding

Fig.2 - Schema Sorgente di Verita con ObjectBinding.

Lo schema delle nostre sorgenti di verità e valori derivati, deve essere aggiornato di conseguenza.


Un esempio pratico

Proviamo a mettere in pratica i concetti sopra esposti con un esempio pratico. Nell'articolo precedente avevamo configurato la nostra CustomTableViewCell per utilizzare una variabile passata per valore contentente una struct denominata Person.


struct CustomTableViewCell : View {
    var model: Person
    
    var body: some View {
    ...

Questo approccio non permetteva di rendere indipendente la CustomTableViewCell rispetto ad eventuali aggiornamenti del suo contenuto; La variabile, infatti, era copiata durante la fase di costruzione della nostra CustomTableViewCell. Ogni cambiamento al suo contenuto era gestito attraverso una variabile di tipo @State di proprietà della ContentView che rigenereva ogni volta la nuova Cella con il nuovo contenuto.

StateVsObjectBindingReference

Fig.3 - Il funzionamento del passaggio per valore rispetto al passaggio per referenza.

Attraverso l'uso del concetto di @ObjectBinding, possiamo fare in modo che il modello Person sia uno soltanto passando alla CustomTableViewCell una referenza ad esso come illustrato nel grafico sopra a destra.

La prima cosa da fare è modificare il nostro modello Person per utilizzare una classe conforme al protocollo BindableObject al posto di una struttura.


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 protocollo BindableObject implementando la proprietà didChange attraverso una istanza di PassthroughSubject.

Ogni proprietà interessata da possibili cambiamenti dell'interfaccia visuale (photoName, title e subtitle) è stata modificata adeguatamente per reagire attraverso il costrutto didSet offerto da Swift. In questo modo, ogni qualvolta aggiorneremo una delle variabili della nostra classe, la funzione update() verrà invocata inviando, a sua volta, una notifica a tutti i subscribers attraverso l'istanza di PassthroughSubject.

CustomTableViewCell verrà modificata per prendere in ingresso una referenza al modello. Per fare questo utilizziamo il modificatore @ObjectBinding discusso in precedenza.


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()
        }
    }
}

Così facendo la CustomTableViewCell non ha una copia del modello Person, bensì lavora con una referenza alla zona di memoria in cui è conservato il vero modello. Vediamo adesso dove creare il modello da passare come referenza.

La ContentView sarà responsabile della creazione del modello concreto in memoria e del passaggio per referenza 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 del ContentView utilizza un nuovo oggetto denominato List che raggruppa gli elementi al suo interno sottoforma di lista come in una UITableView.

Quella che vediamo è la sua forma più semplice di dichiarazione. Nel prossimo articolo vedremo come rendere dinamici i suoi contenuti in funzione del modello dichiarato.

Come al solito, potete scaricare il codice sorgente dal link sotto.

--A presto!


Scarica il codice sorgente

© Copyright 2019 Sofapps - All Rights Reserved - VAT 05197020877