Observable macro in SwiftUI
La macro Observable in SwiftUI rende un oggetto di tipo reference type osservabile da una View
. Ovvero, ogni qual volta questo viene aggiornato, la view che lo sta utilizzando si aggiornerà in manierà automatica.
Grazie alla macro Observable abbiamo uno strumento in più che ci permetterà di dividere la logica dell'app dalla definizione della User Interface.
In questa lezione del corso SwiftUI Tour utilizzeremo la macro Observable
per creare un'app per la gestione delle nostre finanze. L'app permetterà di aggiungere transazioni e di tener traccia del nostro bilancio (puoi seguire quest'ultima parte nel tutorial youtube).
Cominciamo!
Cos'è una Macro in Swift?
Macro è un concetto relativamente nuovo per il linguaggio Swift. Introdotte con Swift 5.9 (Xcode 15), una macro permette di aggiungere del codice accessorio ad un type, variabile o funzione in maniera totalmente invisibile ai nostri occhi.
Le macro si utilizzando con il simbolo @
seguito dal nome della macro.
Nel caso della macro @Observable
questa aggiunge ad una definizione di tipo class
del codice che permette ad una View
di sottoscriversi alle modifiche che avverranno all'interno dell'oggetto.
Facciamo un esempio con una semplice UI che renderizza un contatore il quale viene aumentato e dimininuito tramite due bottoni. Ipotizziamo d'avere la seguente classe:
@Observable
class Counter {
private(set) var value: Int = 0
func increment() {
value += 1
}
func decrement() {
value -= 1
}
}
Se nella View abbiamo un Text
che legge il valore di value
questo Text
verrà aggiornato ogni qual volta il valore di value
cambia.
Expand macro
La macro aggiunge del codice "nascosto" all'interno della classe e del file che,. puoi farlo selezionando la parola Observable
, poi tasto destro ed infine Expand Macro
:
Il codice espanso della macro @Observable
aggiunge un oggetto chiamato ObservationRegistrar
il quale tiene traccia degli oggetti che si sono sottoscritti per essere avvisati di cambiamenti e rende la classe conforme al protocollo Observable
.
Come utilizzare la macro Observable in una View
Una volta definita una classe di tipo Observable
, se questa viene inizializzata all'interno di una View
dovrà essere marcata dalla property wrapper State
prima di essere utilizzata:
@Observable
class Counter {
private(set) var value: Int = 0
func increment() {
value += 1
}
func decrement() {
value -= 1
}
}
struct CounterView: View {
@State private var counter = Counter()
var body: some View {
VStack {
Text("Counter: \(counter.value.formatted(.number))")
HStack {
Button("+", action: counter.increment)
Button("-", action: counter.decrement)
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
Il alcuni esempi online noterai che è possibile inizializzare un
Observable
senza utilizzareState
. Anche se questo è possibile e comunque ti da funzioni di osservabilità, senzaState
non potrai creareBindable
. Quando inizializzo unObservable
all'interno di unaView
preferisco sempre aggiungereState
in modo tale da rendere subito esplicito il comportamento di quella variabile.
ComputedProperty e macro Observable
Capito che tutte le propietà di tipo var
possono essere osservate, possiamo intuire come anche propietà di tipo computed lo diventano a loro volta.
Per esempio, ipotizziamo d'aggiungere una get
only property chiamata asPoint
che legge il valore di value
e lo moltiplica per 10. Quando questa computed property viene letta da una view, al cambiare delle propietà che questa legge, le view verranno aggiornate correttamente:
@Observable
class Counter {
private(set) var value: Int = 0
var asPoint: Int {
value * 10
}
// resto del codice
}
// all'interno della view
Text("Point: \(counter.asPoint.formatted(.number))")
Observable
funziona anche quando le tue computed property contengono set
, didSet
e willSet
.
Come passare un oggetto Observable tra view
Adesso che abbiamo capito come creare un Observable
in SwiftUI e come inizializzarlo all'interno di una view, non ci resta che capire come questo può essere passato tra views.
Ti lascio una regola generale che ti spero ti aiuti a capire il funzionamento:
Utilizza
let
per osservare. Esempio: Vuoi passare un observable in una view child e vuoi aggiornare qualche view interna a child quando l'observable viene modificato:
@Observable
class User {}
struct MainView: View {
@State private var user = User()
var body: some View {
ChildView(user: user)
}
}
struct ChildView: View {
let user: User
var body: some View {
// cambiamenti in `user` aggiorneranno le view che lo utilizzano
}
}
Bindable
per modificare. Esempio: Hai unaChildView
la quale ha dei campi per la modifica di alcune propietà:
@Observable
class User {
var name = ""
}
struct MainView: View {
@State private var user = User()
var body: some View {
Text("Hello, \(user.name)")
ChildView(user: $user)
}
}
struct ChildView: View {
@Bindable var user: User
var body: some View {
TextField("Name", value: $user.name)
}
}
Environment
quando vuoi passare un observable senza utilizzare i costruttori di view. Questo sistema è utilissimo quando hai observable che sono condivisi da molte view. Le modifiche verranno propagate correttamente a tutte le view che osservano l'environment:
@Observable
class User {
var name = ""
}
struct MainView: View {
@State private var user = User()
var body: some View {
Text("Hello, \(user.name)")
ChildView()
.environment(user)
}
}
struct ChildView: View {
@Environment(User.self) var user: User
var body: some View {
TextField("Name", value: $user.name)
}
}
Conclusione
Grazie alla macro Observable saremo in grado di creare definizioni di type complesse senza troppe preoccupazioni. Se proviene da versioni precedenti di SwiftUI ti ricorderai sicuramente come @Published
causava tantissimi problemi con valori annidati ed Array
. Con Observable
tutti quei problemi sono superati e rimaranno solamente dei brutti ricordi.
Buona programmazione!