simulator · Phone · Apple

Modern iOS (NativeScript shell)

A transitional iOS target: one guest realm in a NativeScript shell on the arm64 simulator.

Development hostGuest · QuickJS + native corehosts/apple/

pocket ios builds against an ios-dev plan and launches inside a NativeScript shell on an arm64 iOS simulator, over the pocket-apple crate: one guest realm, one UI surface, the software rasterizer driven incrementally through a damage tracker behind a small C ABI, and a UIKit view compositing only what changed. The published @nativescript/pocketjs plugin carries a prebuilt PocketApple.xcframework, so the default flow needs no Rust toolchain.

The profile stays out of POCKET_TARGETS until it has device-level acceptance — the same convention the iPhone 2G target used. The iPod touch and iPhone 4S hosts reuse the same engine/apple core on real hardware.

Processor
Apple silicon Mac (simulator)
Memory
host RAM
Display
density 1–4, default 3
An iPhone 14 Pro, top-down front view
An iPhone 14 Pro, top-down front view. Photo: iGeeksBlog · CC BY-SA 4.0
01

Hardware

curated here · sources below

Host

Runs on
An Apple silicon Mac with Xcode and an arm64 iOS 16+ simulator runtime
Shell
NativeScript app (hosts/apple/ns-shell) with @nativescript/pocketjs 0.2.1
Guest modes
Sidecar QuickJS realm, or the NativeScript runtime itself as the guest engine
Profile
ios-dev / host ABI 7; 120 Hz path published by the native bridge
02

PocketJS on this machine

Guest · QuickJS + native core

What runs

pocket ios doctor checks Xcode, the simulator runtime, node and the ns CLI; pocket play ios nsengine resolves the manifest against ios-dev, runs the build from the plan, stages the bundle, pak and plan into the shell and launches ns run ios on a booted simulator. Density is load-bearing: glyph atlases bake at build time and the shell sets the surface's raster scale from the staged plan.

What is proven

Simulator boot, rendering at 60 fps, touch with aspect-fit inverse mapping, and the guest↔host service round trip in both guest modes. A real-device run is not yet exercised.

Record

  1. v0.10.0

    Modern iOS runs guests as a transitional target inside a NativeScript shell on the simulator.

  2. v0.10.1

    The 120 Hz path completes with @nativescript/pocketjs 0.2.1.

Profile · as declared by the demo manifestsource ↗
Profile
ios-dev
03

Example code

upstream source · highlighted at build time
apps/nsengine/app.tsxtsx · 90 lines · @6c43f49

The demo that pings the host service from the guest.

// @title NS Engine — pocketjs guest talking to a NativeScript host
import { createSignal, onMount } from "solid-js";
import { Image, Screen, Text, View } from "@pocketjs/framework/components";
import { runEffect } from "@pocketjs/framework/effects";
import { createSpriteAnimation, onFrame } from "@pocketjs/framework/lifecycle";
import { pumpHostLines } from "./channel.ts";

const SPINNER_FRAMES = [
  "spinner-00.svg",
  "spinner-01.svg",
  "spinner-02.svg",
  "spinner-03.svg",
  "spinner-04.svg",
  "spinner-05.svg",
  "spinner-06.svg",
  "spinner-07.svg",
];

// Bakes the glyphs dynamic host strings may use (digits, punctuation).
const GLYPH_SEED = "0123456789 #:{}\"pong hello from NativeScript,.!?-_iOS via sandboxed realm";

// In a sidecar realm (Direction A) no platform globals exist; when the
// NativeScript runtime is the guest engine (Direction B), the whole iOS
// surface is one identifier away.
declare const UIDevice: { currentDevice: { systemVersion: string } } | undefined;
const platformReach = typeof UIDevice !== "undefined"
  ? `iOS ${UIDevice!.currentDevice.systemVersion} via NativeScript`
  : "sandboxed realm";

function Stat(props: { label: string; value: string; valueClass: string }) {
  return (
    <View class="flex-col flex-1 gap-1 rounded-lg bg-slate-800 p-3 shadow">
      <Text class="text-xs text-slate-400 tracking-wide">{props.label}</Text>
      <Text class={props.valueClass}>{props.value}</Text>
    </View>
  );
}

export default function App() {
  const [reply, setReply] = createSignal("waiting");
  const [hostEvent, setHostEvent] = createSignal("none yet");
  const [count, setCount] = createSignal(0);
  const spinnerSrc = createSpriteAnimation(SPINNER_FRAMES, { frameStep: 5 });

  onFrame(() => pumpHostLines((message) => {
    setHostEvent(String(message["msg"] ?? JSON.stringify(message)));
  }));

  const ping = () => {
    const n = count() + 1;
    setCount(n);
    runEffect("ns.ping", { n }, (result) => setReply(String(result)));
  };

  // Fire one round trip unprompted so the channel proves itself on boot.
  onMount(() => ping());

  return (
    <Screen class="relative flex-col w-full h-full overflow-hidden justify-between p-5 bg-gradient-to-b from-slate-950 to-slate-900">
      <View class="flex-row items-center justify-between">
        <View class="flex-row items-center gap-3">
          <Image class="w-10 h-10 rounded-lg shadow" src="logo.png" />
          <View class="flex-col">
            <Text class="text-base text-white font-bold tracking-wide">
              PocketJS × NativeScript
            </Text>
            <Text class="text-xs text-slate-400 tracking-wide">
              one Rust core · two JS worlds
            </Text>
          </View>
        </View>
        <Image class="w-8 h-8" src={spinnerSrc()} />
      </View>

      <View class="flex-row gap-3">
        <Stat label="guest → host" value={reply()} valueClass="text-xs font-bold text-cyan-400" />
        <Stat label="host → guest" value={hostEvent()} valueClass="text-xs font-bold text-blue-400" />
        <Stat label="platform" value={platformReach} valueClass="text-xs font-bold text-emerald-400" />
      </View>

      <View class="flex-row items-center gap-4">
        <View class="rounded-xl bg-blue-600 px-5 py-2 shadow" focusable onPress={ping}>
          <Text class="text-sm font-bold text-white">Ping host · {count()}</Text>
        </View>
        <View class="h-1 flex-1 rounded bg-gradient-to-r from-blue-500 to-cyan-500" />
      </View>
      <Text class="text-slate-900">{GLYPH_SEED}</Text>
    </Screen>
  );
}
04

Bring-up guide

upstream documents · rendered verbatim

Toolchain, build, deploy and acceptance are owned by pocket-stack/pocketjs. The documents below are rendered from the pinned checkout without edits; relative links point back into the repository at the same revision.

Modern iOS via NativeScript

4 min read · 805 words

Status table, one-time setup, build and run, the ios-dev profile.

rendered verbatim fromdocs/APPLE.md@ 6c43f49raw ↗

pocket ios runs PocketJS guests on the iOS simulator inside a NativeScript shell app. The native core is engine/apple (PR #255): the pocket-apple crate behind a C ABI, and PocketSurfaceView, a UIKit view driving one guest realm and one software-rastered surface per instance. The NativeScript side is the published @nativescript/pocketjs plugin, whose npm package carries a prebuilt PocketApple.xcframework — the default flow needs no Rust toolchain.

Current status

ClaimEvidence
Guest boots, renders, animates at 60 fpsiOS 26.5 simulator, apps/nsengine at density 4
Touch reaches the guest with aspect-fit inverse mappingPing host pressable increments on tap
Guest ↔ host service round tripns.ping → shell reply renders in the guest stat tile, unprompted on mount
External-guest mode (the app's JS runtime is the guest engine)Guest code reads UIDevice.currentDevice.systemVersion
Platform-contract identity enforced end to endPlan-built bundles bake ios-dev/7 and mount only on hosts publishing the same pair
Real-device runNot yet exercised — simulator only

One-time setup

pocket ios doctor    # Xcode, arm64 iOS 16+ simulator runtime, node, ns CLI
pocket ios setup     # adds the two Rust iOS targets (only needed for --rebuild-native)

An Apple Silicon Mac is required. PocketApple.xcframework and the @nativescript/ios-quickjs runtime ship ios-arm64/ios-arm64-simulator slices only, so the shell excludes x86_64 for simulator builds (hosts/apple/ns-shell/App_Resources/iOS/build.xcconfig). CocoaPods is not required: neither the shell nor the plugin carries a Podfile.

Build and run a demo

pocket play ios nsengine                     # build, stage, launch on the simulator
pocket ios play nsengine --external-guest    # the NativeScript runtime as the guest engine
pocket ios build nsengine --density=4        # guest artifacts only (dist/ios/nsengine/)
pocket ios devices                           # admissible simulators

The flow: resolve the app's manifest against the ios-dev profile → run tools/build.ts from the plan → stage <output>.pocketjs, <output>.pak, <output>.plan.json and current.json into the shell's src/assets/pocket/npm install (first run) → boot an arm64 simulator → ns run ios --device <udid> --no-hmr --justlaunch. The shell runs in place, so only the first run pays the full cost: ~47 s cold (npm install + full Xcode build, Apple Silicon, warm simulator) and ~16 s on repeat runs (--no-build; ~25 s with a guest rebuild). --attach keeps ns run attached for console output.

Density is load-bearing: glyph atlases bake at build time, and the shell sets the surface's raster scale from the staged plan — a guest built at one density and rastered at another renders soft text. --density=1..4, default 3.

The ios-dev profile

tools/ios-profile.ts follows the transitional pattern (tools/iphone2g-profile.ts): a scoped registry that stays out of POCKET_TARGETS until the host has device-level acceptance. Profile: platform ios, form embedded (a fixed 480×272 logical viewport letterboxed by the view), presentations native + integer-fit, capabilities input.touch + text.glyphs.baked only — PocketSurfaceView reports no buttons and a centered analog.

The identity contract: bundles built from a resolved plan bake __POCKET_TARGET__/__POCKET_HOST_ABI__ and refuse to mount unless the host publishes the same pair (framework/src/host.ts). Three places publish "ios-dev" / 7 and must stay in agreement: this profile, PocketSurfaceView.m (pocket_apple_set_identity at init), and the plugin's external-guest ui mount. tests/ios-profile.test.ts guards the first two.

The two guest modes

  • Sidecar (default, PocketView) — the guest runs in the QuickJS realm embedded in the xcframework. The host app's runtime never sees guest code; the surface composes into the app's layout like any UIView.
  • External guest (--external-guest, PocketHostView) — the shell's own JS runtime evaluates the bundle; globalThis.ui delegates each op over the NativeScript metadata bindings to the same native core. Guest code reaches the whole iOS platform with no per-API glue.

Both modes run the identical bundle; current.json selects the view class.

The shell (hosts/apple/ns-shell)

Authored and committed: package.json, nativescript.config.ts, webpack.config.js, tsconfig.json, references.d.ts, src/app.ts, App_Resources/iOS/{build.xcconfig,Info.plist,LaunchScreen.storyboard}. Generated and gitignored: node_modules/, platforms/, hooks/, src/assets/pocket/, package-lock.json. The shell is plan-driven — it reads the staged plan for viewport and density and the staged mode for the view class, so nothing is templated at stage time. Its tsconfig.json pins @nativescript/core paths so the plugin's typings resolve when the plugin is a file: symlink. --shell-dir=<path> stages into another NativeScript app instead.

Pre-publish overrides

--plugin-path=<checkout> and --runtime-tgz=<tgz> point the shell at a local @nativescript/pocketjs checkout and a local @nativescript/ios-quickjs tarball. The committed package.json names the published packages; overrides are applied for the npm install and the template is restored afterwards. With --plugin-path, a present engine/apple/dist/PocketApple.xcframework is copied into the local plugin (--rebuild-native rebuilds it first).

Sources

  • engine/apple/ — pocket-apple crate, PocketSurfaceView, build-xcframework.sh
  • tools/ios.ts, tools/ios-profile.ts — the CLI flow and the profile
  • hosts/apple/ns-shell/ — the committed shell
  • apps/nsengine/ — the reference guest (service channel + platform probe)
  • @nativescript/pocketjs — the plugin repo
Photo

Wikimedia Commons · IPhone 14 Pro.jpg — iGeeksBlog, CC BY-SA 4.0.