Shared State Across SwiftUI and WidgetKit
How the SwiftUI app, WidgetKit, caches, and deep links carry the same park state
Hangangjari’s iOS implementation connects the app, widgets, SwiftData cache, App Group snapshots, deep links, and external map-app handoff in one flow. An App Group snapshot is a small bundle of display-ready values shared by the app and widgets.
The app supports short checks before and during a visit to the Han River. A widget shows a frequently visited park, and tapping it opens the same park and screen in the app.
Hangangjari is checked in short moments: before leaving home, before taking the subway, near a parking-lot entrance, or while looking for facilities inside a park. The client had to arrange API values and carry state between those moments.
Even if the network is briefly slow or a widget is reading an old snapshot, the screen has to explain the state so users do not misunderstand it.
The iOS implementation first defined where shared state is stored and how it is read again. API responses are stored in SwiftData, and the app state store reshapes them into a form the UI can use.
Widgets read App Group snapshots, and deep links carry the selected park and screen back into the app. Together they preserve context across the two runtimes.
Code Shared by App and Widget
The client is currently split into three targets.
Hangangjari: the SwiftUI main app.HangangjariWidget: parking and general park widgets.HangangjariTests: tests for model, networking, cache, and widget logic.
The app and widgets share model, networking, repository, localization, and widget snapshot code. The WidgetKit extension is not the same process as the app, so values that both sides need are written separately into the App Group area.
flowchart LR View["SwiftUI view"] --> Store["App state store\n@Observable @MainActor"] Store --> ParkingRepo["Parking repository"] Store --> ForecastRepo["Forecast repository"] ParkingRepo --> API["API client"] ForecastRepo --> API ParkingRepo --> Cache["SwiftData cache"] ForecastRepo --> Cache Store --> Snapshot["Widget snapshot store"] Widget["WidgetKit provider"] --> Loader["Widget entry loader"] Loader --> Snapshot Loader --> API Loader --> Location["Widget location resolver"] Settings["Widget / favorite settings"] --> AppGroup["App Group UserDefaults"] Widget --> AppGroup API --> Backend["FastAPI /v2"]
Screen State Coordinated in One Place
The app state store gathers the values each screen needs and delegates network and storage work to repositories.
The current state contains these groups.
- Park list.
- Parking-lot lists per park and the latest status per parking lot.
- Outing overview per park.
- First-screen
HomeSummary. - Parking forecast overview and timeline.
- Park congestion forecast overview and timeline.
- Screen state derived from favorites, widgets, and deep links.
The app state store does not handle HTTP implementation details directly. Repositories own API calls and SwiftData persistence, while the store builds screen state transitions and display shapes.
The store retains screen-state coordination. Separate components handle URL construction, request proof, decoding, and SwiftData row upsert.
Inside the store, I tried to leave only UI-ready state such as “this park is being viewed,” “this value is loading,” and “this snapshot can be displayed.”
A Repository Between Cache and API
The parking repository handles parks, lots, statuses, outing overview, and home summary. The forecast repository handles forecast config, parking forecasts, park congestion forecasts, and timeline payloads.
Repositories handle API calls, local cache freshness decisions, response storage, and widget snapshot updates.
sequenceDiagram
autonumber
participant View as SwiftUI view
participant Store as App state store
participant Repo as Repository
participant Cache as SwiftData cache
participant API as API client
participant Snapshot as Widget snapshot store
View->>Store: Request first-screen values
Store->>Repo: Check stored first-screen values
Repo->>Cache: Read cached response
alt Cache is fresh
Cache-->>Repo: Usable first-screen value
Repo-->>Store: Screen model from cache
else Stale or missing
Repo->>API: Request first-screen response
API-->>Repo: Decoded first-screen response
Repo->>Cache: Store home / parking / forecast values
Repo-->>Store: Fresh screen model
end
Store->>Snapshot: Update parking/general widget display values
Store-->>View: Update screen state
With this approach, the first screen does not wait for several APIs one by one. The server sends a screen-ready response, the app preserves it in SwiftData, and then the app splits it into shapes for the screen and widgets.
The repository, cache, and snapshot steps provide a specific inspection point when the app reopens, a widget cannot reach the network, or the server adds a field.
WidgetKit Outside the App Process
Hangangjari has two widget types.
- Parking widget: recommended parking lot, remaining spaces, status, and nearby sorting.
- General park widget: congestion, weather, event and facility signals, and information for deciding whether to go today.
The widget reads the stored snapshot first. If it is fresh, it uses it. If it is missing or old, it calls the API. If that fails, it falls back to a stale snapshot or placeholder.
flowchart TB Provider["Widget timeline provider"] --> Request["Widget load request"] Request --> Loader["Widget entry loader"] Loader --> Cached["App Group snapshot"] Loader --> API["home-summary / forecast / status API"] Loader --> Location["Selected widget location"] API --> Builder["Snapshot builder"] Cached --> Entry["Widget display entry"] Builder --> Entry Entry --> Timeline["Timeline\nrefresh policy"]
The parking widget and general widget use separate snapshot keys. Even for the same park, the first thing each widget should say is different.
A Common API Shape for App and Widget
The API client calls the v2 app API. Write requests or sensitive requests have a path for attaching an app access token and request proof.
For app access validation, I kept these boundaries.
- The app and widgets share the same API client structure.
- Bootstrap flow is separated from protected
v2app APIs. - Whether unsigned fallback is allowed on failure is controlled by runtime policy.
- Write requests such as telemetry and push subscription go through stronger validation than read requests.
App Group and Deep Link State
The WidgetKit extension runs separately from the app. App Group snapshots, repositories, screen-ready server responses, and deep links therefore have to carry the park and state the user just saw together.
The server and app interpret fresh, stale, and unavailable as the same UI states. Value freshness, retry availability, and the park selected by a deep link have to align for short checks before and during a visit to remain continuous.
App Group snapshots and deep links connect information checked in a widget to the corresponding detail screen in the app.
Comments
No comments yet. Be the first to leave one.
Pending review