Skip to Content
DocsPlatformsiOS

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

DriverMechanismOwns
Native@klera/native-driver-ios — Expo Module, UIKit-directtap / swipe / type / scroll / tapCoord / multiTap / pinch / screenshot / clipboard / system UI
idb fallbackMeta’s iOS Device Bridge, subprocessThe same surface, minus multi-touch and pasteboard — for Expo Go hosts that cannot link a native module
Sim-hostxcrun simctl, subprocesssetLocation / 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:

  • setLocationxcrun simctl location <udid> set <lat>,<lon>.
  • setBiometricxcrun simctl spawn <udid> defaults write com.apple.BiometricKit_Sim Enrolled -bool YES plus a notifyutil event arming the next prompt as success / failure.
  • relaunchxcrun simctl terminate then simctl launch (with optional CLI args), optionally followed by simctl openurl for 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-ios

Wrap 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.

npx expo prebuild --platform ios npx expo run:ios

Verify the wiring

pnpm exec klera doctor

Expected:

✓ 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 detected

If 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

ActionNativeidbSim-host
tap / type / scrollruntime 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.

Next steps

Last updated on