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
ProgressViewsopra 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 (
ObjectIdentifierdella 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
CIContextGPU (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.