Interactive widgets in SwiftUI, and the writes that never land
A button in a widget looks like one line of code. The store it writes to is the interesting part.
Interactive widgets are the single feature that changed this app the most, and the sample code makes them look trivial. Put a Button with an AppIntent in your widget, tick a habit, done. It is trivial right up to the moment you check whether the tick is actually in your database, and sometimes it is not.
The reason is that the widget extension is a separate process with its own view of storage. If your store lives in the app’s container the widget cannot see it, so it goes in an app group. If the app is running in the background it may be holding a context that has no idea a write happened elsewhere. And if you reload the wrong timeline, or forget to reload one at all, the widget shows the correct data on the next system refresh, which might be forty minutes later, and the user concludes the tap did nothing.
There is also a budget. Widget reloads are rationed, and an app that reloads on every write gets throttled, which produces exactly the stale widget you were trying to avoid.
This post covers the app group setup, the intent that performs the write, cross-process change notification, which timelines to reload and when, and how to test the whole thing without becoming convinced you are losing your mind.
Outline
Status: outline. The introduction above is finished copy. Everything below is the section plan for the full draft, targeted at about 1 600 words.
- What changed in iOS 17. AppIntent in a widget, and the process boundary it crosses.
- App groups. Store location, and the migration if yours is in the wrong place.
- The write intent. The code that ships, with the error handling that matters.
- Cross-process notification. Getting the app to notice a widget write.
- Reloading timelines. Which ones, when, and the budget you are spending.
- Testing it. A method that catches the failures the simulator hides.