cat swift/mainactor-aggiornamenti-ui.md

Lezione 3550 min

MainActor e aggiornamenti UI

La UI deve essere aggiornata nel contesto corretto: MainActor rende esplicito l'isolamento del main thread.

Nelle app con interfaccia, aggiornare stato osservato dalla UI richiede attenzione. In Swift moderno, MainActor è lo strumento principale per dire: questo codice deve essere isolato al contesto principale.

Non è solo una regola UIKit o SwiftUI. È una parte del modello di concurrency.

Annotare un ViewModel

@MainActor
final class ProductListViewModel {
  private(set) var prodotti: [Prodotto] = []
  private(set) var isLoading = false

  func load() async {
    ...
  }
}

Tutti i membri isolati della classe devono essere usati sul MainActor.

Questo rende più sicuro aggiornare proprietà lette dalla UI.

Lavoro di rete fuori, aggiornamento UI dentro

@MainActor
final class ProductListViewModel {
  private let api: ProductAPI
  private(set) var prodotti: [Prodotto] = []
  private(set) var errore: String?

  init(api: ProductAPI) {
    self.api = api
  }

  func load() async {
    do {
      let prodotti = try await api.prodotti()
      self.prodotti = prodotti
    } catch {
      self.errore = String(describing: error)
    }
  }
}

Anche se load è MainActor-isolated, l’operazione await api.prodotti() può sospendersi. L’importante è che gli aggiornamenti dello stato del view model avvengano nel contesto corretto.

MainActor.run

Se sei in codice non isolato e devi aggiornare UI:

await MainActor.run {
  self.prodotti = prodotti
}

Ma se puoi annotare il tipo o il metodo con @MainActor, spesso è più chiaro.

Metodo isolato

@MainActor
func mostra(_ prodotti: [Prodotto]) {
  self.prodotti = prodotti
}

Da fuori:

await mostra(prodotti)

Il chiamante vede che entra in isolamento MainActor.

Non mettere tutto su MainActor per pigrizia

Annotare tutto con @MainActor può essere comodo, ma non tutto deve vivere li.

Buono:

  • ViewModel UI;
  • stato osservato dalla UI;
  • metodi che aggiornano UI.

Non ideale:

  • parsing pesante;
  • calcoli lunghi;
  • cache condivise;
  • networking service puro.

Il dominio e i servizi dovrebbero restare indipendenti dal MainActor quando possibile.

Approachable concurrency

Le versioni recenti di Swift hanno lavorato per rendere la concurrency più progressiva e meno rumorosa, specialmente per codice UI e script. Ma il principio resta: sapere dove vive lo stato e chi può modificarlo.

MainActor non è un cerotto. È una dichiarazione di isolamento.

Task in UI

In SwiftUI spesso avrai:

Task {
  await viewModel.load()
}

Oppure .task { ... }.

Il task avvia lavoro asincrono; il view model isolato al MainActor gestisce stato UI.

Cancellazione UI

Se l’utente lascia la schermata, il lavoro dovrebbe fermarsi. In SwiftUI .task viene gestito in base al ciclo di vita della view, ma nei tuoi view model devi comunque pensare a task salvati e cancellazione quando necessario.

private var loadTask: Task<Void, Never>?

func reload() {
  loadTask?.cancel()
  loadTask = Task {
    await load()
  }
}

Checklist

Usa @MainActor per:

  • tipi che rappresentano stato UI;
  • funzioni che aggiornano proprietà osservate;
  • confini in cui devi garantire main thread.

Evita di usarlo per:

  • dominio puro;
  • networking puro;
  • parsing pesante;
  • cache è actor di storage.

Chiediti sempre:

  • questo stato è letto dalla UI?
  • chi lo modifica?
  • posso isolare solo il view model?
  • sto spostando lavoro pesante sul main actor?

Esercizio

Crea un @MainActor ProductListViewModel con:

  • prodotti;
  • isLoading;
  • errore;
  • func load() async.

Il metodo deve:

  • impostare loading;
  • chiamare una API async;
  • aggiornare prodotti;
  • gestire errori;
  • ignorare cancellazione senza mostrare errore.