simctl Integration¶
β Implemented π§ͺ Tested π± Simulator Only
Current state:
simctlintegration is fully implemented for simulator lifecycle, app management, device discovery, and demo mode. macOS only. See the Status Glossary for chip definitions.
AutoMobile uses simctl for iOS simulator lifecycle and app management. This layer is
responsible for booting simulators, installing apps, launching processes, and controlling
system-level simulator behaviors.
Responsibilities¶
- Simulator lifecycle: boot, shutdown, erase.
- App lifecycle: install, uninstall, launch, terminate.
- Device discovery and capability reporting.
- Status bar configuration (demo mode) when supported.
- Live locale and localization changes (see below).
- Biometric (Touch ID / Face ID) simulation for
biometricAuth(see below). - Simulated push delivery for
postNotification(see below). - Deep-link / URL-scheme discovery from the installed
.appbundle (see below). - Per-app notification authorization read via BulletinBoard (see below).
- Device settings capture/restore for
deviceSnapshot(see below).
Live locale changes¶
β Implemented
The changeLocalization MCP tool supports live locale changes on iOS simulators and
physical iOS devices. Simulators use simctl plus defaults; physical devices use the
lockdownd com.apple.international domain for language/locale reads and writes.
How it works¶
- Write simulator settings via
xcrun simctl spawn <udid> defaults write: AppleLocaleβ the ICU locale identifier (underscored form, e.g.ja_JP).AppleLanguagesβ an ordered array of BCP 47 tags built from the requested locale (e.g.["ja-JP", "ja"]), so UI strings resolve correctly.AppleTimeZone,AppleICUForce24HourTime,AppleCalendarβ for the remaining localization axes.- Read-back verification β every write is read back and compared to the expected value; a mismatch is reported as an error.
- SpringBoard restart β
launchctl stop com.apple.SpringBoardinside the simulator causes SpringBoard to re-launch and adopt the new preferences. The server pollslaunchctl list com.apple.SpringBoard(up to 10 retries, 500 ms apart) to confirm it is back. - Darwin notification β
notifyutil -p com.apple.language.changedtells in-process observers that the locale has changed. - Optional app restart β the
restartAppparameter accepts a bundle ID. When provided, the server terminates the app withxcrun simctl terminateand relaunches it withxcrun simctl launch, so the app picks up the new locale (running apps cache locale at launch). The bundle ID is validated against a strict reverse-DNS pattern to prevent shell injection.
Physical iOS devices¶
Physical devices cannot be targeted with simctl spawn. AutoMobile reads and writes the
lockdownd com.apple.international domain instead:
Languageβ the primary language code, e.g.ja.Localeβ the ICU locale identifier, e.g.ja_JP.
The device must be USB-connected, unlocked, paired, and trusted. Reads use
ideviceinfo; writes use a lockdownd setter backend. The default command backend expects
pymobiledevice3 for SetValueForDomain writes and reports an actionable setup error if
that command is unavailable. The adapter verifies every physical-device locale write by
reading Locale back through lockdownd.
Changing language/region on a real device restarts SpringBoard as part of iOS applying
the setting, so AutoMobile does not run simulator SpringBoard restart commands on
physical devices. restartApp is simulator-only.
MCP tool parameters (iOS-specific)¶
| Parameter | Description |
|---|---|
locale |
Locale tag, e.g. ar-SA, ja-JP |
timeZone |
IANA zone ID, e.g. Asia/Tokyo |
timeFormat |
"12" or "24" |
calendarSystem |
e.g. gregory, japanese, buddhist |
restartApp |
Bundle ID of an app to terminate and relaunch after the change (iOS only) |
Limitations¶
- On physical iOS devices, only
localeis writable through the lockdownd language/region path. timeZoneis not supported on physical iOS devices; iOS exposes no known lockdownd key for this setting.timeFormatis not supported as an independent physical-device write; 12h/24h behavior is locale-derived unless changed manually in Settings.calendarSystemis not supported as an independent physical-device write. When a calendar is encoded in the locale string, AutoMobile can read it back from that locale.- Text direction (
textDirectionparameter) is not applicable on iOS β RTL layout is driven by the app’s language; set an RTL locale instead. - Running apps cache locale at launch. Use the
restartAppparameter or manually relaunch the app for it to pick up new settings;restartAppis simulator-only.
Biometric simulation¶
β Implemented π§ͺ Tested π± Simulator Only
The biometricAuth MCP tool simulates Touch ID / Face ID on simulators by posting
BiometricKit Darwin notifications inside the simulator via
xcrun simctl spawn <udid> notifyutil. This reaches parity with the Android emulator
adb emu finger path (see Android biometrics) for
match / fail.
How it works¶
- Enroll β a registered
notifyutilset/read/post oncom.apple.BiometricKit.enrollmentChangedensures a biometry is enrolled. The registration is required becausenotifyutil -shas no effect when no process has registered the key. - Match / non-match β post the key the app’s
LAContextis waiting on: - Touch ID:
com.apple.BiometricKit_Sim.fingerTouch.match/β¦nomatch - Face ID:
com.apple.BiometricKit_Sim.pearl.match/β¦nomatch
modality: "any" posts both pairs; the simulator’s non-enrolled biometry is a no-op,
so the action works regardless of the device’s biometry type.
Action mapping¶
MCP action |
iOS result |
|---|---|
match |
post *.match |
fail |
post *.nomatch |
cancel / error |
supported: "partial" β no simctl equivalent |
Limitations¶
- Simulator only. Physical iOS devices have no public biometric-injection API and return
supported: false. cancel/errorcannot be injected β the notifications carry only match vs non-match. On Android these use theAutoMobileBiometricsSDK override, which has no iOS counterpart.
Push notifications¶
β Implemented π§ͺ Tested π± Simulator Only
The postNotification MCP tool delivers a simulated remote push to a target app on a
booted simulator via xcrun simctl push <udid> <bundleId> <payload.apns>. Unlike Android
(a local notification posted by the in-app SDK; see
Android notifications), this is an OS-routed simulated push
that needs no AutoMobile iOS SDK β but it does require an explicit appId (bundle id).
How it works¶
- Build an APNs payload:
title/bodyβaps.alert,channelIdβaps.category, plus a top-level"Simulator Target Bundle": <appId>. - Reject payloads larger than the 4096-byte
simctl pushlimit. - Write the payload to a temp
.apnsfile and runsimctl push(the simctl command layer has no stdin, so a file is used rather than-).
Limitations¶
- Simulator only.
simctl pushcannot target physical devices (no APNs token/server); physical iOS devices returnsupported: false. appIdis required on iOS β there is no frontmost-bundle resolution like the Android dumpsys path.- Rich media is limited:
bigPicture/imagePath(needs a Notification Service Extension) andactions(need a pre-registeredUNNotificationCategory) are ignored with awarningrather than failing.
Deep-link discovery¶
β Implemented π§ͺ Tested π± Simulator Only
The getDeepLinks MCP tool returns an iOS app’s declared deep links. iOS apps declare
deep links statically in bundle metadata rather than via a runtime resolver, so AutoMobile
reads them off the installed .app bundle on disk. Custom URL schemes and universal-link
hosts come from two different sources.
How it works¶
- Resolve the bundle path β
xcrun simctl get_app_container <udid> <bundleId> appreturns the.app’s host filesystem path. A missing app exits non-zero and is reported as a clean “not installed” result. - URL schemes β read
<app>/Info.plistwith hostplutil -convert jsonand collectCFBundleURLTypes[].CFBundleURLSchemes[](deduplicated). These become theschemesfield. - Universal-link hosts β
codesign -d --entitlements :- <app> | plutil -convert jsonsurfaces the code-signing entitlements; AutoMobile keepscom.apple.developer.associated-domainsentries prefixed withapplinks:and strips the prefix. These become thehostsfield. Unsigned bundles or bundles without associated domains yield[], not an error. - Document types β best-effort
supportedMimeTypesfromCFBundleDocumentTypes[].LSItemContentTypes[]. - Cross-platform shape β synthesized
intentFilters(one VIEW filter listing each scheme/host) keep theDeepLinkResultshape identical to Android so platform-agnostic callers work unchanged.
get_app_container routes through simctl; plutil/codesign are host tools (not
simctl subcommands) and run directly on the host filesystem.
Limitations¶
- Simulator only. Physical-device discovery (copying the device-signed bundle off-device
via
devicectl) returns an explicit “not yet implemented” error. - iOS has no runtime intent resolver, so only declared schemes/domains are reported.
supportedMimeTypesis best-effort document-type metadata, not a routing guarantee.- Unsupported under host control (Docker/external-emulator mode):
get_app_containerresolves to a macOS host path, butplutil/codesignrun inside the container, so the call returns an explicit unsupported error instead of a misleading failure.
Notification authorization read¶
β Implemented π§ͺ Tested π± Simulator Only
The getNotificationPolicy MCP tool reports per-app notification authorization
(UNAuthorizationStatus) on iOS simulators by reading the BulletinBoard daemon’s on-disk
section info. There is no host-side runtime API for this, so AutoMobile decodes the
persisted plist.
How it works¶
- Locate the plist β
~/Library/Developer/CoreSimulator/Devices/<udid>/data/Library/BulletinBoard/VersionedSectionInfo.plist. - Decode the outer plist β convert it with
plutil -convert xml1(notjson: the file embeds<data>blobs JSON cannot represent) and extract the base64<data>blob registered undersectionInfo[<bundleId>]. - Decode the nested blob β each section value is a separate
bplist00NSKeyedArchiver archive. Because it containsCFKeyedArchiverUIDrefs thatplutil -convert jsonrejects, AutoMobile writes the blob to a temp file and converts it withplutil -convert xml1, then reads the settings dict scalars (authorizationStatus,alertType,lockScreenSetting,notificationCenterSetting,pushSettings). - Map the status β
authorizationStatusmaps tonotDetermined/denied/authorized/provisional/ephemeral;allowedis true for any delivery-capable status βauthorized(2),provisional(3, quiet delivery) andephemeral(4, App Clips). Callers needing strict full authorization checkauthorizationStatus === "authorized". The result usesmethod: "ios_bulletinboard_plist".
An app with no registered section returns allowed: null plus a warning (the app likely
never requested authorization), not an error. A missing/unreadable plist returns a warning
rather than throwing.
Limitations¶
- Simulator only. Physical devices return
supported: false(no host-side read API; notification settings are only available to the owning app at runtime viaUNUserNotificationCenter). - Read-only.
setNotificationPolicystays unsupported on iOS: there is no public API to write per-app notification authorization, and editing the BulletinBoard plist on disk would not take effect without restarting the daemon. - This is per-app authorization status, a different concept from Android’s DND policy access β see Notifications.
- Unsupported under host control (Docker/external-emulator mode): the BulletinBoard plist
lives on the macOS host but
plutilruns inside the container, so the read returns an explicitsupported: falserather than reporting every app as unknown.
Device settings snapshot¶
β Implemented π§ͺ Tested π± Simulator Only
When deviceSnapshot is invoked with includeSettings: true on an iOS simulator,
AutoMobile captures and restores a curated allowlist of simulator settings alongside app
data. (Previously iOS silently dropped settings and hard-coded the manifest to
includeSettings: false.)
How it works¶
- Capture per-key defaults β for each
(domain, key)in the allowlist, runxcrun simctl spawn <udid> defaults read <domain> <key>and record the value. The allowlist is scalar-only for now ({.GlobalPreferences, AppleLocale}); array-typed keys such asAppleLanguagesare a follow-up because a plain per-keydefaults writecannot round-trip them. - Capture UI state β
xcrun simctl ui <udid> appearanceand... content_sizeread the device-level light/dark appearance and Dynamic Type size. - Persist to the manifest β captured values go into a distinct optional
iosSettingsmanifest field (separate from Android’sglobal/secure/systemtriplet, which does not fit(domain, key)exports). - Restore surgically β on restore (gated on
manifest.includeSettings && manifest.iosSettings, identical to Android), each value is re-applied with a per-keydefaults write(never a whole-domaindefaults import, so system-managed keys are not clobbered), thensimctl ui appearance/content_sizere-apply UI state so it takes effect without a respawn.
Individual key read/write failures are logged and skipped (non-fatal), matching the per-package resilience of iOS app-data restore.
Limitations¶
- Simulator only; physical-device settings are not reachable via
simctl. - Scalar keys only in the first cut; array-typed and per-app domains are future work.
simctl spawn ... defaultshas no stdin, so whole-domaindefaults export/importis not used β the per-key allowlist is the deliberate, auditable design.
Do Not Disturb (setDeviceState)¶
β Implemented π± Simulator Only
The setDeviceState / getDeviceState MCP tools control Do Not Disturb. On iOS,
DND is simulator-only and binary (on/off) β this is a hard platform
limitation, not a missing wiring detail.
How it works¶
- Binary toggle β the only lever the simulator exposes is the
com.apple.donotdisturb.enabledDarwin notification, driven viaxcrun simctl spawn <udid> notifyutil: notifyutil -1 com.apple.donotdisturb.enabled ...creates a temporary registration. This is required becausenotifyutil -shas no effect when no process has registered the key.notifyutil -s com.apple.donotdisturb.enabled <0|1>sets the value while the registration is alive.notifyutil -g com.apple.donotdisturb.enabledverifies the registered state in the same invocation.notifyutil -p com.apple.donotdisturb.enabledposts it so observers react.- Honest capability reporting β every result carries a machine-readable
capabilityfield so callers can branch instead of string-matching warnings: binaryβ simulator: on/off only.unsupportedβ physical device: DND cannot be set at all.- (
fullis reserved for Android, where all fourzen_modetiers are distinct, persisted, and verified.) - No silent downgrade β a
priorityoralarmsrequest applies plain DND and reports it honestly:requestedMode: "priority"|"alarms",appliedMode: "none", a structuredwarning, andverified: false(sosuccessisfalse). The tool never claims a tier it cannot deliver. - Best effort β results are
bestEffort: true.notifyutilvalues are registration-scoped, so the setter verifies inside the registered invocation. A later standalone-gread may not reflect a prior-s; the readback is advisory, not authoritative.
Limitations¶
- Simulator only. Physical iOS devices return
supported: false,capability: "unsupported", and a specific error: iOS exposes no public API to enable/disable Focus or Do Not Disturb (only the read-only Focus Filter API), and Apple’s device tooling (devicectl, XCUITest) ships no DND/Focus setter. - Binary, not per-mode. Since iOS 15, DND lives inside the private Focus
framework. There is no per-mode (priority vs. alarms-only) Darwin notification
analogous to Android’s
zen_modeinteger, so iOS cannot be mapped to the Android four-mode model. - No cosmetic fallback.
simctl status_bar overrideexposestime/dataNetwork/wifi*/cellular*/battery*/operatorNameβ no DND flag β so even a status-bar-only indicator is unavailable.
Usage patterns¶
- Prefer deterministic simulator selection by device identifier.
- Keep simulator state consistent between runs (reset/erase when needed).
- Use dedicated simulators for parallel test execution.
Limitations¶
- macOS only (requires Xcode Command Line Tools).
- Simulator-only; physical devices are out of scope for simctl.
See also¶
- CtrlProxy iOS - Touch injection and element queries.
- iOS overview
- Android biometrics - The
biometricAuthAndroid counterpart. - Android notifications - The
postNotificationAndroid counterpart, and the cross-platform notification policy concept (Android DND policy access vs iOS per-app authorization).