iOS
The driver layer for iOS Simulator and physical devices.
klera resolves every iOS device action through a three-driver
composite. Capabilities route to the driver that can actually fulfil
them — there is no fall-through chain of three failed attempts before a
call lands. If a flow asks for setLocation, the call goes straight to
xcrun simctl. If it asks for tapCoord, it goes straight to the
in-process Expo Module. The fork is in one place; the engine never
branches.
The three drivers
| Driver | Mechanism | Owns |
|---|---|---|
| Native | @klera/native-driver-ios — Expo Module, UIKit-direct | tap / swipe / type / scroll / tapCoord / multiTap / pinch / screenshot / clipboard / system UI |
| idb fallback | Meta’s iOS Device Bridge, subprocess | The same surface, minus multi-touch and pasteboard — for Expo Go hosts that cannot link a native module |
| Sim-host | xcrun simctl, subprocess | setLocation / setBiometric / relaunch (with args and deep-link URL) |
createCompositeDeviceDriver({
primary: native,
fallback: idb,
routes: {
setLocation: simHost,
setBiometric: simHost,
relaunch: simHost,
},
})That wires the three together. primary.tap → fallback.tap is the
standard ladder; the routed capabilities skip the ladder and dispatch
directly to the sim-host slot.
Native driver — primary
The native driver runs in-process. Each DeviceAction lands on a
Swift method in the linked Expo Module: dismissAlert, swipe,
screenshot (UIKit drawHierarchy(in:afterScreenUpdates:)),
setOrientation, setClipboard / getClipboard (UIKit
UIPasteboard.general), openURL, backgroundApp / foregroundApp,
dismissKeyboard, tapCoord, multiTap, pinch. Multi-touch
gestures fire concurrent UITouch sequences inside one UIEvent so
UITapGestureRecognizer.numberOfTouchesRequired > 1 sees them as one
gesture.
Because the driver is in-process, every action runs at UIKit speed — no subprocess, no XCUITest harness, no companion daemon.
idb fallback — Expo Go path
If a runtime registers without a linked native driver, the composite
falls back to Meta’s idb (brew tap facebook/fb && brew install idb-companion && pipx install fb-idb). idb covers swipe,
screenshot, openURL, and the dismissAlert walk-the-AX-tree
pattern. It explicitly rejects multi-touch, pasteboard I/O, and
orientation — those need an in-process API. Adopters who hit those
limits move from Expo Go to a custom dev client.
Sim-host driver — xcrun simctl
Three capabilities can only be done from outside the app process:
setLocation→xcrun simctl location <udid> set <lat>,<lon>.setBiometric→xcrun simctl spawn <udid> defaults write com.apple.BiometricKit_Sim Enrolled -bool YESplus anotifyutilevent arming the next prompt as success / failure.relaunch→xcrun simctl terminatethensimctl launch(with optional CLI args), optionally followed bysimctl openurlfor a deep link.
The runtime’s bridge connection drops during termination and reconnects on its own.
Bridgeless / Fabric is required
klera v0.6+ requires React Native’s New Architecture (Bridgeless /
Fabric, default since RN 0.74). The runtime probes
global.RN$Bridgeless at start; on Old Architecture it refuses to
register and emits a migration link instead of silently mis-walking
the fiber tree.
// @klera/runtime — at registration time
if (typeof global.RN$Bridgeless === "undefined") {
console.error(
"klera requires React Native New Architecture. " +
"Set newArchEnabled to true in app.json and rebuild your dev client."
);
return; // do not register the runtime
}If you see “klera runtime did not connect” after a rebuild, check
app.json:
{
"expo": {
"newArchEnabled": true
}
}Setting up
Install the native driver
klera init does this for you. If you’re wiring it in by hand:
pnpm add -D @klera/runtime @klera/native-driver-iosWrap your app root
// App.tsx
import { KleraRuntimeProvider } from "@klera/runtime";
import KleraDriver from "@klera/native-driver-ios";
export default function App() {
return (
<KleraRuntimeProvider driver={KleraDriver}>
<YourApp />
</KleraRuntimeProvider>
);
}The Expo Module’s app.plugin.js wires the iOS pod automatically. If
you’re using bare workflow without Expo’s config plugins, run
pod install after adding the package.
Rebuild the dev client
Native modules need a custom dev client — Expo Go cannot load them.
Expo prebuild
npx expo prebuild --platform ios
npx expo run:iosVerify the wiring
pnpm exec klera doctorExpected:
✓ Node ≥ 20 v20.x.x
✓ iOS Simulator 1 booted (iPhone 16 Pro · iOS 18.0)
✓ DeviceDriver native linked + reachable
✓ DeviceDriver sim-host xcrun simctl reachable
✓ New Architecture Bridgeless detectedIf DeviceDriver native reports not linked, the dev client is the
last-built JS bundle without the Expo Module compiled in — rebuild
with expo run:ios.
Common gotchas
First-launch TCC prompts. A fresh simulator profile shows system
permission prompts (camera, microphone, location) the first time an
app requests them. The native driver cannot dismiss them — they’re
rendered by tccd, outside the app process. Pre-grant with
xcrun simctl privacy <udid> grant <service> <bundle-id> before
klera run, or assert against your own in-app fallback UI.
Simulator version. The sim-host driver targets the first Booted
row from xcrun simctl list devices booted. If multiple simulators
are booted you’ll get the first one in the list — pin a specific UDID
in .klera/config.yaml or shut the others down.
Xcode Command Line Tools. xcrun simctl ships with Xcode, not the
standalone Command Line Tools package. xcode-select -p should point
into Xcode.app, not /Library/Developer/CommandLineTools. If
klera doctor reports xcrun not found, run
sudo xcode-select -s /Applications/Xcode.app.
idb companion lifecycle. idb requires a companion process per
target. idb connect <udid> is idempotent — calling it on an
already-connected simulator is a no-op. If the companion gets stuck
(rare), idb kill && idb connect <udid> cycles it.
Deep links during relaunch. simctl openurl runs after simctl launch returns, which is before the JS bundle finishes loading. If
your URL handler races the runtime’s reconnect, add a waitForIdle
before the assertion that depends on the deep link’s effect.
Capability matrix
| Action | Native | idb | Sim-host |
|---|---|---|---|
tap / type / scroll | runtime fiber walker (not a driver call) | — | — |
swipe | ✓ | ✓ | — |
tapCoord | ✓ | ✗ (coord-space mismatch) | — |
multiTap | ✓ | ✗ | — |
pinch | ✓ | ✗ | — |
screenshot | ✓ | ✓ | — |
setClipboard / getClipboard | ✓ | ✗ (out-of-process) | — |
setOrientation | ✓ | ✗ | — |
dismissAlert / dismissActionSheet | ✓ | ✓ | — |
dismissKeyboard | ✓ | ✗ | — |
backgroundApp / foregroundApp | ✓ | ✗ | — |
openURL | ✓ | ✓ | — |
setLocation | ✗ | ✗ | ✓ (routed) |
setBiometric | ✗ | ✗ | ✓ (routed) |
relaunch | ✗ | ✗ | ✓ (routed) |
pressBack | ✗ (Android-only) | ✗ | ✗ |
grantPermission | ✗ (Android-only) | ✗ | ✗ |
✗ entries surface as capability_unsupported errors with a hint
naming the driver that does support the call.