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:

Come espandere una macro in Xcode. Seleziona la parola Observable, tasto destro e tap su 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 utilizzare State. Anche se questo è possibile e comunque ti da funzioni di osservabilità, senza State non potrai creare Bindable. Quando inizializzo un Observable all'interno di una View preferisco sempre aggiungere State 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 una ChildView 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!