Interactive home/lock-screen widget buttons (count +1, timer start/stop)
gave touch feedback but often didn't persist, only working after several
rapid taps. Two compounding defects in the widget-extension write path:
- The extension could crash mid-write. refresh()/makeContainer() opens a
new ModelContainer on every intent perform and every timeline read; in
the extension that hit a local-store branch whose failure was fatalError.
A transient App Group store contention with the running main app crashed
the whole appex — the tap looked dead and only a later retry won the race.
Split makeContainer into a throwing makeContainerThrowing(); refresh() now
wraps it in try? and keeps the previous container on failure, so the
extension degrades instead of crashing. The app's DataStore.shared keeps
its crash-on-failure semantics.
- Fetch → mutate → save could span different contexts and save errors were
swallowed. IntentStore.context is a computed container.mainContext, and
commit() re-read it for a try? save(), so a mid-flight container swap left
the mutation on a dead context and any save failure vanished silently.
Added IntentStore.performWrite(_:) which captures one context for the whole
fetch+mutate+save, saves with a retry (no silent try?), then fires the
Live Activity hook and reloadAllTimelines. All four mutating intents
(RunAction/StartTime/StopTime/AddCount) now fetch in that single context.
No @Model or main-app logic changed — fix is confined to the widget/intent
data layer. Verified: 5 rapid RunActionIntent.perform() calls persist +5 with
no lost writes across three cold launches (ok=true each). Build succeeds
(app + embedded Haru_DanimWidgets.appex).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7