State, events and navigation
Reactive stores
Apsule uses three conceptual scopes:
| Scope | Lifetime | Intended use |
|---|---|---|
state | View/component runtime | UI state, loading flags, current list data. |
global | Running Apsule Project | Session/account/shared state across routes. |
storage | Persistent | Settings and data that survive process/project restarts. Accessed through Host API rather than magical synchronous disk writes. |
Changes to state and global are reactive:
state.loading = true;
global.currentUser = user;Bound SwiftUI nodes should update automatically.
Actions
JSON references named actions; it should not contain arbitrary JavaScript:
{
"id": "refresh",
"type": "Button",
"props": { "text": "Refresh" },
"events": {
"tap": { "action": "refreshMatches" }
}
}actions.refreshMatches = async () => {
state.loading = true;
try {
state.matches = await api.getMatches();
} finally {
state.loading = false;
}
};Actions may accept an optional event payload as their first JavaScript argument. Most ordinary static controls currently invoke actions without domain data. Controls rendered inside ForEach receive the active iteration scope, so an action can read values such as event.match.id or event.item.content without generating a separate static action for every row.
The entry view may define an appear action. The Host invokes it through the normal Promise-aware action path when the running Project root appears, which makes it the preferred place for asynchronous cache loading and initial synchronization that must publish state back to SwiftUI.
Event vocabulary
Implemented event vocabulary includes:
appeardisappearforegroundinactivebackgroundtapdoubleTaplongPresschangesubmitrefreshdragdropswipefocusblurconfirmreordercontextnavigateerrormessageregionChangemapTap- top-level actions named
openURL,notification, andpushfor external lifecycle events
Events carry JSON-serializable payloads. Gesture/map/web events place event-specific fields under event; ForEach also preserves the active iteration scope.
Navigation
Prefer route names over embedding filesystem paths throughout views:
{
"routes": {
"home": "views/home.json",
"player": "views/player.json"
}
}JavaScript API:
router.push("player", { id: player.id });
router.pop();
router.replace("home");
router.sheet("settings");
router.dismiss();The JavaScript router above is implemented. Route parameters are read-only inputs under the route.params scope unless copied into state/global. Declarative NavigationLink also accepts props.params.
The Host additionally handles its apsule:// deep-link scheme. A running Project receives ordinary external URLs through actions.openURL(payload). Scene transitions invoke top-level actions.foreground, actions.inactive, or actions.background. Project-push notifications invoke actions.push(payload) only when the Project declared and received the push permission; local-notification taps invoke actions.notification(payload).
This route model is also suitable for a future visual navigation graph in Apsule Builder.
Subscriptions
Long-lived Host API operations return cancellable subscriptions:
const sub = host.location.watch(position => {
global.position = position;
});
// Later
sub.cancel();The runtime should automatically dispose view-scoped subscriptions when their owning view is destroyed unless explicitly promoted to app scope.