Questo report documenta perché il numero di sigarette, l’azione di aggiunta e i log non risultano sincronizzati tra app iOS, widget e app watchOS, e propone un piano di correzione concreto.
- Origine unica dei dati (SwiftData, App Group) presente nell’app, ma il widget non la usa: legge/scrive chiavi UserDefaults diverse e non coerenti.
- Il pulsante “+” del widget attualmente non inserisce nessuna sigaretta nel database condiviso (intent placeholder o coda “pending” mai processata).
- L’app e il Watch scambiano dati via App Group + WatchConnectivity, ma il widget resta fuori da questo circuito.
- Esistono due implementazioni di widget nel target, con logiche tra loro differenti e parzialmente scollegate.
Conseguenza: aggiungendo una sigaretta dall’app il totale diventa 12, ma il widget continua a mostrare un valore diverso (o non si aggiorna), e lo stesso vale in direzione opposta.
-
App iOS
- Persistenza: SwiftData nel container App Group
group.fightthestroke.mirrorsmoker(vediPersistenceController.swift). - Sync centrale:
SyncCoordinatoraggiorna widget e Watch quando cambiano i dati (chiavi condivise eWidgetCenter.reloadAllTimelines()). - Esporta verso App Group UserDefaults: salva
todayCount,lastUpdatede un JSON dei log giornalieri in chiavi del tipocigarettes_yyyy-MM-dd(vediSyncCoordinator.updateSharedUserDefaults()).
- Persistenza: SwiftData nel container App Group
-
Watch app
- Legge/scrive su App Group UserDefaults (
group.fightthestroke.mirrorsmoker) e usaWCSessionper sync in tempo reale (fileMirrorSmokerStopper Watch App/SharedDataManager.swifteWatchConnectivityManager.swift). - Chiavi usate:
todayCount,lastUpdated,cigarettes_yyyy-MM-dd.
- Legge/scrive su App Group UserDefaults (
-
Widget (target “MirrorSmokerStopper Widget”)
- Widget attivo:
MirrorSmokerWidget(inMirrorSmokerStopper Widget/MirrorSmokerStopper_Widget.swiftcon@maininMirrorSmokerStopper_WidgetBundle.swift). - Sorgente dati:
WidgetStoreche legge/scrive chiavi proprie:widget_today_count,widget_last_cigarette_time,widget_pending_cigarettes. - Azione “+”: usa
AddCigaretteIntent(placeholder) che non inserisce nulla nel database condiviso. - Seconda implementazione presente ma non principale:
CigaretteWidget(usaQuickAddFromWidgetIntent), scrive inwidget_pending_cigarettesma la coda non viene mai processata in app.
- Widget attivo:
- Mismatch delle chiavi tra App/Watch e Widget
- App e Watch usano chiavi App Group
todayCount,lastUpdated,cigarettes_yyyy-MM-dd(vediSyncCoordinator.updateSharedUserDefaults()eWatch App/SharedDataManager). - Il widget legge chiavi proprie:
widget_today_count,widget_last_cigarette_time(vediMirrorSmokerStopper Widget/WidgetStore.swift). - Non esiste alcun punto in cui l’app aggiorni
widget_today_count, quindi il widget non riflette i salvataggi reali.
- Azione “+” del widget non inserisce alcuna sigaretta nel database condiviso
MirrorSmokerWidgetusaAddCigaretteIntent(fileAddCigaretteIntent.swift), che esplicitamente non inserisce: è un placeholder con commento “In a real app, you would…”.- La seconda via (
QuickAddFromWidgetIntent) aggiunge un timestamp alla codawidget_pending_cigarettes, ma:processPendingCigarettes(modelContext:)inWidgetStoreè uno stub e non fa nulla.- Non c’è alcun consumer lato app che legga/flush la coda
widget_pending_cigarettesin SwiftData.
- Due implementazioni di widget nel target, con logica incoerente
MirrorSmokerWidget(attivo) usa intent placeholder e chiaviwidget_*.CigaretteWidget(presente ma non nel bundle @main) usaQuickAddFromWidgetIntente ancoraWidgetStorecon coda pending.- Nessuna delle due implementazioni accede al ModelContainer condiviso via App Group per leggere/scrivere il dato reale.
- Il widget non usa la stessa “fonte unica” usata da App e Watch
- L’app scrive i dati verità in SwiftData (App Group) e, a beneficio del Watch, propaga un riassunto in UserDefaults (App Group).
- Il widget non legge né SwiftData (via
AppGroupManager.sharedModelContainer) né le stesse chiavi UserDefaults (todayCount,cigarettes_yyyy-MM-dd) usate da Watch.
- Refresh cronologico ok, ma senza dati corretti non sincronizza
WidgetCenter.reloadAllTimelines()viene chiamato in vari punti (app, widget), ma il widget continua a leggere chiavi non aggiornate → ricarica con valori sbagliati.
- Aggiunta dall’app:
Cigaretteviene inserita in SwiftData;SyncCoordinatoraggiornatodayCountnei UserDefaults condivisi e invia reload ai widget. Il widget però mostra ancora il vecchio valore perché leggewidget_today_countche non viene mai scritto dall’app. - Aggiunta dal widget (bundle attivo):
AddCigaretteIntentnon salva nulla → nessun cambiamento né in app, né in Watch, né nel widget stesso. - Aggiunta dal widget (implementazione alternativa): il timestamp finisce nella coda
widget_pending_cigarettes, mai consumata → nessun effetto in app/Watch; il widget può indicare “pending/syncing” ma il conteggio reale non cambia. - Aggiunta dal Watch: l’app riceve via
WCSessiono via UserDefaults condivisi, aggiorna SwiftData etodayCount; il widget resta disallineato per lo stesso motivo del punto 1.
-
App (SwiftData + Sync)
MirrorSmokerStopper/Utilities/PersistenceController.swiftMirrorSmokerStopper/Utilities/SyncCoordinator.swift→updateSharedUserDefaults()scrivetodayCount,lastUpdated,cigarettes_yyyy-MM-ddin App Group UD.MirrorSmokerStopper/Views/ContentView.swift→ salvaCigarettee chiamasyncCoordinator.cigaretteAdded(from: .app, ...).
-
Watch
MirrorSmokerStopper Watch App/SharedDataManager.swift→ usa App Group UD con le chiavi:todayCount,lastUpdated,cigarettes_yyyy-MM-dd.MirrorSmokerStopper Watch App/WatchConnectivityManager.swift→ WCSession per sync live.
-
Widget
MirrorSmokerStopper Widget/MirrorSmokerStopper_Widget.swift(+ bundle @main) → provider legge daWidgetStore.MirrorSmokerStopper Widget/WidgetStore.swift→ chiaviwidget_today_count,widget_last_cigarette_time,widget_pending_cigarettes;processPendingCigarettesè vuoto.MirrorSmokerStopper Widget/AddCigaretteIntent.swift→ placeholder (nessun inserimento dati).MirrorSmokerStopper Widget/CigaretteWidget.swift→ implementazione alternativa non principale conQuickAddFromWidgetIntent(scrive solo pending).- Esiste già un accesso al ModelContainer condiviso, ma nel modulo app:
MirrorSmokerStopper/Utilities/AppGroupManager.swiftconWidgetDataProvider.addCigaretteFromWidget()che inserisce correttamente in SwiftData (non usato dal widget attivo).
Priorità Alta (per allineare subito i conteggi):
- Unificare la fonte dati del widget
- Opzione A (consigliata): far leggere al widget i dati direttamente da SwiftData nel contenitore App Group.
- Usare
AppGroupManager.sharedModelContainernel provider per calcolaretodayCounte “last cigarette time”. - Vantaggio: una sola fonte verità per App/Widget/Watch, nessun doppio canale.
- Usare
- Opzione B (workaround veloce): se si preferisce restare su UserDefaults, far leggere al widget le stesse chiavi del Watch (
todayCounte decodifica dicigarettes_yyyy-MM-ddper l’ora dell’ultima).- Aggiornare il provider del widget per usare
UserDefaults(suiteName: group)con chiavitodayCount/cigarettes_...invece diwidget_today_count.
- Aggiornare il provider del widget per usare
- Implementare l’azione “+” del widget per inserire davvero
- Collegare il pulsante “+” a un Intent che:
- accede a
AppGroupManager.sharedModelContainere inserisce una nuovaCigarette(come faWidgetDataProvider.addCigaretteFromWidget()). - al termine chiama
WidgetCenter.reloadAllTimelines()e aggiorna un flag/lastUpdatedin App Group UD per triggerare la UI app/Watch.
- accede a
- Eliminare l’intent placeholder (
AddCigaretteIntentcosì com’è) o sostituirne l’implementazione.
- Rimuovere o completare la coda “pending”
- Se si passa a inserimento diretto nel ModelContainer condiviso, rimuovere completamente
widget_pending_cigarettese i relativi metodi. - In alternativa, implementare realmente
processPendingCigarettes(modelContext:)e una logica in app che la richiami in foreground/background per flushare la coda in SwiftData (ma è più fragile e ridondante rispetto all’inserimento diretto condiviso).
- Consolidare a una sola implementazione di widget
- Tenere solo
MirrorSmokerWidgeto soloCigaretteWidget, ma allineata con i punti 1–3. Evitare doppioni che confondono flussi/chiavi.
Priorità Media (pulizia e robustezza):
- Allineare le chiavi, se si mantiene un canale UD
- Se resta l’uso di UD per “last time” visibile nel widget, far scrivere ad
updateSharedUserDefaults()anchewidget_last_cigarette_time(opzionale se si legge SwiftData direttamente).
- Verificare tempi di aggiornamento
- Tenere
WidgetCenter.reloadAllTimelines()nei punti chiave (post-save app, post-intent widget, post-sync Watch) – già presente – e ridurre policy del timeline provider solo se necessario.
- Nel target Widget, sostituire
WidgetStorecon un provider che usaAppGroupManager.sharedModelContainerper leggeretodayCounte l’ultima sigaretta del giorno, similmente aWidgetDataProvider.getTodayStats(). - Implementare l’intent di aggiunta nel widget richiamando
WidgetDataProvider.addCigaretteFromWidget()(già pronto nel codice, ma non utilizzato dal widget attivo). - Rimuovere
AddCigaretteIntentplaceholder o implementarlo davvero con inserimento in SwiftData condiviso. - Eliminare
widget_pending_cigarettese metodi collegati se si adotta l’inserimento diretto. - Facoltativo: se per qualsiasi motivo si preferisce UD, far leggere al widget
todayCounte calcolare l’ora dell’ultima dal JSON incigarettes_yyyy-MM-dd(chiavi già scritte daSyncCoordinator).
- Aggiungo una sigaretta dall’App →
todayCounte lista in SwiftData si aggiornano; il widget legge dalla stessa fonte e mostra 12; il Watch riceve via WCSession/UD e mostra 12; il widget rilegge e mostra 12. - Aggiungo una sigaretta dal Widget → la
Cigaretteviene inserita in SwiftData (App Group); l’App (stesso store) e il Watch (via UD/Sync) mostrano 12; il widget rilegge e mostra 12. - Aggiungo una sigaretta dal Watch → l’App salva,
SyncCoordinatoraggiorna UD e ricarica widget; widget e App mostrano 12.
- Tutti i target coinvolti dichiarano l’App Group
group.fightthestroke.mirrorsmokernei rispettivi.entitlements(coerente). - Persistenza SwiftData dell’App è correttamente collocata nell’App Group (compatibile con condivisione al widget).
-
Widget
MirrorSmokerStopper Widget/MirrorSmokerStopper_WidgetBundle.swift(bundle @main)MirrorSmokerStopper Widget/MirrorSmokerStopper_Widget.swift(widget attivo)MirrorSmokerStopper Widget/WidgetStore.swift(chiaviwidget_*, coda pending non usata)MirrorSmokerStopper Widget/AddCigaretteIntent.swift(placeholder)MirrorSmokerStopper Widget/CigaretteWidget.swift(widget alternativo non @main)
-
App
MirrorSmokerStopper/Utilities/PersistenceController.swift(SwiftData in App Group)MirrorSmokerStopper/Utilities/SyncCoordinator.swift(updateSharedUserDefaults()→ chiavi condivise per Watch)MirrorSmokerStopper/Views/ContentView.swift(inserimentoCigarettee trigger sync)MirrorSmokerStopper/Utilities/AppGroupManager.swift(WidgetDataProvider.addCigaretteFromWidget()già pronto per inserire dal widget)
-
Watch
MirrorSmokerStopper Watch App/SharedDataManager.swiftMirrorSmokerStopper Watch App/WatchConnectivityManager.swift
Il disallineamento nasce da due problemi principali: 1) il widget non usa la stessa fonte dati/chiavi dell’app e del Watch; 2) l’azione di aggiunta del widget non scrive nel database condiviso. Unificando la lettura/scrittura del widget al container SwiftData in App Group (o alle stesse chiavi UD) e collegando davvero l’intent di aggiunta all’inserimento in SwiftData, App/Widget/Watch mostreranno sempre lo stesso conteggio e lo stesso storico, con aggiornamenti in tempo reale.