Embedded App
EmbeddedApp drives the runtime without the full platform event loop. Use it for embedding the Native SDK in an existing application, game engine, or custom render loop.
Usage
var embedded = native_sdk.embed.EmbeddedApp.init(my_app.app(), my_platform);
try embedded.start();
// On app activation/deactivation:
try embedded.activate();
try embedded.deactivate();
// In your render loop:
try embedded.frame();
// On resize:
try embedded.resize(new_surface);
// On shutdown:
try embedded.stop();Methods
| Method | Description |
|---|---|
init(app, platform) | Create an embedded app with a runtime |
start() | Dispatch app_start event, loads the WebView source |
activate() | Dispatch app_activated event |
deactivate() | Dispatch app_deactivated event |
resize(surface) | Dispatch surface_resized event |
frame() | Dispatch frame_requested event |
stop() | Dispatch app_shutdown event |
How it works
EmbeddedApp wraps a Runtime and an App. Each method dispatches a platform event via runtime.dispatchPlatformEvent, giving you full control over the event loop while still using Native SDK's runtime, bridge, and window management.
Mobile examples
Mobile support is experimental as a whole, embedding included: everything here is real and verified — iOS on the simulator, Android on the emulator — but mobile APIs and tooling may still change, and desktop is the mature surface. See Platform Support.
The repository includes full mobile host examples:
examples/ios- Xcode project with a SwiftUIViewController,WKWebView, andnative_sdk.hbridge.examples/android- Gradle/Kotlin project with JNI and CMake wiring forlibnative-sdk.a.examples/mobile-canvas- canvas-scene mobile app built withaddMobileLib: the user'sModel/Msg/update/viewapp compiles into the embed static library, renders through the CPU reference renderer, and is presented by a minimal iOS or Android host shim with touch, keyboard, and IME forwarding.
The iOS and Android shell examples expect a local libnative-sdk.a built from the repository and copied into the path documented in each example README; the canvas example builds its own library through its build.zig.
Mobile host contract
Mobile embedding uses the same runtime lifecycle through a thin native host:
| Host event | Native SDK call | Current examples |
|---|---|---|
| View/controller creation | native_sdk_app_create, configure packaged assets if needed, then native_sdk_app_start | Swift viewDidLoad; Android onCreate |
| Packaged asset source | native_sdk_app_set_asset_root and native_sdk_app_set_asset_entry before startup | Generated package hosts point the runtime at the bundled asset root and manifest frontend entry |
| App activation lifecycle | native_sdk_app_activate, native_sdk_app_deactivate | Swift SceneDelegate active/resign-active callbacks; Android onResume/onPause |
| Content size or surface changes | native_sdk_app_resize, then native_sdk_app_frame | Swift viewDidLayoutSubviews; Android surfaceChanged with orientation/screen-size changes handled in the same activity |
| Touch input | native_sdk_app_touch, then native_sdk_app_frame | Android forwards MotionEvent data; iOS keeps touch handling in UIKit/WebKit for now |
| Keyboard and inset changes | Host layout update, then native_sdk_app_resize and native_sdk_app_frame when the surface changes | Swift keyboard frame notifications adjust the WKWebView constraint; Android uses adjustResize |
| Native commands | native_sdk_app_command | Mobile chrome can dispatch stable command IDs such as mobile.back and mobile.refresh through the embedded runtime |
| Shutdown | native_sdk_app_stop, then native_sdk_app_destroy | Swift controller deinit; Android onDestroy |
The mobile examples keep navigation/header UI in UIKit or Android views and keep rich content in WKWebView or Android WebView. Native header buttons and the Android system Back action dispatch command IDs through native_sdk_app_command, so mobile chrome can use the same command/event model as desktop menus, shortcuts, and native controls. App activation, safe areas, orientation, keyboard avoidance, back gestures, and platform lifecycle ownership stay in the native host. Keyboard changes are handled by native layout first and forwarded to the Native SDK as resize/frame work when the content surface changes. Mobile ShellView metadata maps to generated native host config for the header labels, command buttons, and WebView workspace; dynamic runtime view mutation remains a desktop API for now.
Declared platform chrome
Apps can declare platform chrome — a tab set and optionally one primary floating action — in shell metadata (ShellConfig.chrome in a Zig scene, .shell.chrome in app.zon):
const scene: native_sdk.ShellConfig = .{
.windows = native_sdk.embed.mobile_shell_scene.windows,
.chrome = .{
.tabs = &.{
.{ .id = "tabs.albums", .label = "Albums", .icon = "app:albums" },
.{ .id = "tabs.songs", .label = "Songs", .icon = "music" },
},
.primary_action = .{ .id = "action.new", .label = "New", .icon = "plus" },
},
};On iOS the toolkit host projects the declaration as REAL native controls: an actual system tab bar and a real button, styled and made accessible by the OS — never imitated in canvas. Icons come from the icon vocabulary (built-in names, or app:<name> for icons the app registers) and are rasterized through the same vector core the canvas draws with into template images the system tints. The bar's overlap is folded into the safe-area insets the host already reports, so layout clears it through the existing on_chrome channel with no new code.
Selection state lives in the app's model. A tab tap dispatches the tab's declared command id through native_sdk_app_command — the same path as native header buttons — and the app maps it to a Msg in on_command; the model's selected_tab_fn derivation drives which item the bar shows active. The bar is a projection of the model, never the source of truth, so the whole loop is deterministic and replays from the Msg journal.
Where nothing projects the declaration it is honestly inert: desktop hosts today ignore it (desktop apps compose their own chrome in canvas), and Android projection is a later round. An app that also composes an in-canvas tab switcher can yield it while the chrome channel's tabs_projected flag is set, so exactly one tab affordance is ever on screen.
The same chrome channel carries the host-reported form factor (WindowChrome.form_factor): the iOS host reports its horizontal size class (compact/regular), so apps that switch shells can prefer host truth and keep a width derivation as the fallback for hosts that never report.
Platform push/pop navigation
Apps whose model owns a page stack (a list opening into a detail) can project it as REAL platform navigation transitions. The seam is two options beside selected_tab_fn:
// Depth follows the VISIBLE page: 0 = the root, 1 = one push in. Tab
// switches are lateral, never depth — derive the current tab's stack.
pub fn navigationDepth(model: *const Model) usize {
return if (model.tab == .albums and model.open_album != null) 1 else 0;
}
// In mobileOptions():
.navigation_depth_fn = navigationDepth,
.navigation_back_command = "nav.back",The MODEL owns navigation state; the platform contributes the transition, never the state. The iOS host polls the depth once per tick: when it grows by one the host presents the system push transition, and when it shrinks by one the system pop. Both sides of a transition are exact frames of the two model states — the outgoing page is the previous frame (retained from the damage path with zero extra renders), and the incoming state is rendered through the ordinary damage delivery before the animation starts, so the transition is pixel-correct for its whole run and the live canvas swaps back onto the top page the moment it lands. A depth change that arrives together with a selected-tab change is a lateral tab switch and reconciles with no transition, and so does any multi-level jump — standard platform behavior, no theater. Rapid changes coalesce the same way: the stack converges to the model's current depth rather than replaying every intermediate hop.
The interactive edge-swipe-back is the system's own recognizer with its real physics, armed exactly while the depth is above zero and a navigation_back_command exists. Completing the gesture dispatches that command through the embed command path exactly once — the same journal entry the app's own back button produces when both map to the same Msg in on_command — and cancelling it dispatches nothing, leaving the model untouched. During the gesture the revealed page is the snapshot retained at push time (one full-resolution frame per depth level, bounded), which can be stale if the shallow page changed underneath; a fresh render replaces it the moment the gesture completes. If the app ignores the dispatched back command, the next poll sees the un-popped depth and pushes the page back — model truth wins, the tab-bar snap-back rule applied to navigation.
Because the depth is a pure derivation and the gesture enters through the ordinary command path, the whole loop stays deterministic: a journal replayed without a host produces the identical model, and two hosts driven by the same journal project the identical stack. Where nothing projects the declaration it is honestly inert: desktop hosts ignore it (desktop apps compose their own navigation in canvas), and Android projection is a later round — the Android system back gesture will ride the same back command when it lands.
Testing with EmbeddedApp
var null_platform = native_sdk.NullPlatform.init(.{});
var state: u8 = 0;
var embedded = native_sdk.embed.EmbeddedApp.init(.{
.context = &state,
.name = "embedded",
.source = native_sdk.WebViewSource.html("<p>Embedded</p>"),
}, null_platform.platform());
try embedded.start();
// null_platform.loaded_source now contains the loaded HTML