Architettura

SwiftUI + ViewModel osservabile, elaborazione Vision/Core Image in un actor dedicato.

Panoramica

FaceOFF è un’app a schermo singolo. Non c’è navigazione multi-pagina né backend: tutto lo stato vive nel ViewModel e tutta la CPU/GPU pesante passa dall’actor di elaborazione.

ContentView (SwiftUI)
  └─ FaceOFFViewModel (@Observable, @MainActor)
       ├─ FaceCensoringService → FaceCensoringProcessor (actor)
       ├─ WatermarkService
       └─ PhotoLibraryService

Stati dell’app

AppState ha tre valori:

  • empty — nessun contenuto; mostra branding Ass. Talea / FaceOFF e il PhotosPicker.
  • processing — elaborazione in corso; overlay con ProgressView sopra l’immagine se già disponibile.
  • preview — risultato pronto; chrome flottante con toggle, slider, undo/redo e salvataggio.

Il passaggio empty → processing → preview avviene in handlePhotoSelection(). Un processingGeneration evita race se l’utente seleziona un’altra foto a metà elaborazione.

ViewModel

FaceOFFViewModel è @MainActor e @Observable (Swift Observation). Possiede:

  • immagine originale + regioni volto cacheate (per ricalcolare solo l’intensità senza ri-eseguire Vision);
  • spot manuali di blur/restore e cronologia (max 10 frame);
  • flag filigrana con cache dell’immagine watermarked (ObjectIdentifier della source);
  • API pubbliche: selezione foto, intensità, pennello, undo/redo, salva, annulla.

displayImage è la proprietà letta dalla UI: se la filigrana è attiva, restituisce la versione con logo, altrimenti l’immagine elaborata.

Actor di elaborazione

FaceCensoringProcessor isola:

  • un CIContext GPU (useSoftwareRenderer: false, cacheIntermediates: false);
  • tutte le chiamate Vision e i filter Core Image;
  • encoding finale con UTI originale e metadata ripuliti dall’orientamento.

La facade FaceCensoringService espone metodi async statici verso l’actor. Così la ViewModel resta sul main thread e non tocca mai Vision direttamente.

Layout iPhone / iPad

ContentView legge horizontalSizeClass:

  • compact — chrome a colonna, altezza immagine ~48% (62% se i comandi sono ridotti);
  • regular — chrome max 520pt, altezza immagine più generosa, toggle affiancati e save/cancel in riga.

PinchZoomImageView è un UIViewRepresentable su UIScrollView (zoom 0.3–6×). In modalità pennello, il tap converte la coordinata in spazio normalizzato CIImage (origine in basso) e la passa al ViewModel.

Permessi

Solo NSPhotoLibraryAddUsageDescription: FaceOFF scrive in galleria con autorizzazione add-only, senza leggere l’intera libreria oltre al picker di sistema.