iOS
Requirements
Section titled “Requirements”- macOS with Xcode installed
xcrun simctlcommand-line tools- Flutter app built for simulator with
--dart-define=PROBE_AGENT=true - Physical devices only:
iproxyfromlibimobiledevicefor USB port forwarding (see Physical Device Setup below)
Simulator Setup
Section titled “Simulator Setup”List available simulators
Section titled “List available simulators”probe device listxcrun simctl list devices availableBoot a simulator
Section titled “Boot a simulator”probe device start --platform iosprobe device start --platform ios --name my-sim # named; created if it does not existEvery simulator or emulator probe opens has a name, and every test result records the device it ran on (name and id/serial; physical devices by model or serial).
Or manually:
xcrun simctl boot <UDID>Build and install your app
Section titled “Build and install your app”flutter build ios --debug --simulator --dart-define=PROBE_AGENT=truexcrun simctl install <UDID> build/ios/iphonesimulator/YourApp.appxcrun simctl launch <UDID> com.example.myappFor cases where Xcode SDK and simulator iOS versions differ, use xcodebuild directly:
xcodebuild -workspace ios/Runner.xcworkspace \ -scheme Runner \ -sdk iphonesimulator \ -configuration Debug \ -destination 'platform=iOS Simulator,id=<UDID>' \ DART_DEFINES=$(echo 'PROBE_AGENT=true' | base64) \ -derivedDataPath build/ios \ buildPhysical Device Setup
Section titled “Physical Device Setup”Testing on a physical iOS device requires USB port forwarding because, unlike simulators, physical devices do not share the host’s loopback network. FlutterProbe uses iproxy (from the libimobiledevice suite) to forward the agent’s WebSocket port over USB.
Install iproxy
Section titled “Install iproxy”brew install libimobiledevice # macOSVerify the installation:
iproxy --helpHow it works
Section titled “How it works”When targeting a physical device, the CLI runs iproxy <host-port> <device-port> -u <UDID> to create a TCP tunnel over USB. The CLI then connects to ws://127.0.0.1:<host-port>/probe?token=... just like it would for a simulator.
Physical iPhone over WiFi (CLI and Studio): what must be true
Section titled “Physical iPhone over WiFi (CLI and Studio): what must be true”WiFi avoids the USB tunnel and its cable mode-flipping, and it is the path Studio’s 📡 button uses. Everything below must hold; the checks at the end tell you which one does not.
- The phone runs a build of your app that contains the agent. The App Store or production build has none: it does not listen on
port 48686 and never will.
ProbeAgent.start()is a no-op unless the app was built with--dart-define=PROBE_AGENT=true, and the agent refuses to start in a release build unless the app passesallowReleaseBuild: true. - Build mode is profile, with your flavor if the project has one, and WiFi is switched on at build time:
Without
Terminal window flutter build ios --profile --flavor <your-flavor> \--dart-define=PROBE_AGENT=true --dart-define=PROBE_WIFI=truePROBE_WIFI=truethe agent listens on localhost only and nothing on the network can reach it. Debug builds cannot be launched from the home screen of a physical iPhone, so use profile. Install withxcrun devicectl device install app --device <UDID> <Runner.app>. - The app is running in the foreground with the screen unlocked. iOS suspends background apps and the listener stops with them.
The agent binds
0.0.0.0on port 48686 (--dart-define=PROBE_PORT=<n>moves it; if the port is busy it takes the next one, up to 48695, and printsPROBE_PORT=<n>). - You have the session token. The agent prints
PROBE_TOKEN=<token>to the app’s console every 3 seconds. It is never advertised on the network. Read it withxcrun devicectl device process launch --console --device <UDID> <bundle-id>, or from Xcode’s console. - The Mac can reach the phone’s IP directly. Both must be on the same network segment. A Mac on Ethernet and a phone on WiFi work only if the router bridges them (no client isolation, no separate guest or IoT network, and for discovery no mDNS filtering between the wired and wireless side). The Mac’s own WiFi being off is fine.
- How the Mac finds the phone:
- CLI, always works:
probe test tests/ --host <phone-ip> --token <token>(add--agent-port <n>if you moved the port). - Studio’s 📡 overlay, manual entry: open Connect manually, type the phone’s IP address or hostname, the port (48686) and the
token from the app’s
PROBE_TOKEN=line, press Test connection, then Connect. This needs no advertiser. See Studio: Manual WiFi entry. - Studio’s 📡 overlay, discovery: it browses the Bonjour service
_flutterprobe._tcpand lists what answers; you pick the device and paste the token. A device only appears when the agent advertises: it only does when the app passes aProbeAdvertiser(for example one built onbonsoir) toProbeAgent.start(advertiser: ...), which on iOS 14+ needs the_flutterprobe._tcpentry inNSBonjourServicesand aNSLocalNetworkUsageDescriptionin that build’sInfo.plist(see Studio: WiFi auto-discovery). Without an advertiser the app logsPROBE_MDNS=off. - Studio’s device picker lists the phone even when it is only paired over the network (as “WiFi only”) and says what it needs; see Studio: the device picker and physical phones.
- Studio over USB (not WiFi): the device picker lists the phone as
physical, uses aniproxytunnel and reads the token from the device log withidevicesyslog(brew install libimobiledevice); the build still has to be profile withPROBE_AGENT=true.
- CLI, always works:
The fastest way to try all of this is the repository’s Flutter fixture app and one script that prints the exact commands for your phone:
scripts/physical-ios-fixture.sh --ip <phone-ip> # steps for the connected iPhone; add --check once the app runsIt lists the connected iPhones, prints the signing step (your Apple developer team), the
flutter run --profile -d <id> --dart-define=PROBE_AGENT=true --dart-define=PROBE_WIFI=true command, the checks below and the CLI and Studio connect steps.
To verify the whole path with Studio in about ten minutes, with the expected result of each step, follow the checklist in the repository
(studio/e2e-web/PHYSICAL.md); its automated half is scripts/studio-e2e-physical.sh --host <phone-ip> --token <token>, opt-in and never run in CI.
Check from the Mac, in this order:
xcrun devicectl list devices # the phone is paired, developer mode on, state "connected"dns-sd -B _flutterprobe._tcp local. # does the app advertise? (only with a ProbeAdvertiser)curl -s http://<phone-ip>:48686/probe/status # {"agent":"flutter_probe","version":"..."}: the agent is reachableThe status endpoint needs no token. If curl is refused or times out: the build has no agent or no PROBE_WIFI=true, the app is not in the
foreground, or the network is blocking the Mac from the phone. If curl answers but dns-sd shows nothing: the app has no
ProbeAdvertiser or mDNS is filtered; use Studio’s manual entry or the CLI --host/--token path.
Connection Flow (Simulator)
Section titled “Connection Flow (Simulator)”The iOS simulator shares the host’s localhost network. No port forwarding is needed.
- The CLI connects directly to
ws://127.0.0.1:<port>/probe?token=... - Token is read via fast path: file at
~/Library/Developer/CoreSimulator/Devices/<UDID>/data/tmp/probe/token - If the file is not available, the fallback streams simulator logs with
xcrun simctl spawn <UDID> log streamand parsesPROBE_TOKEN=lines - The ProbeAgent prints the token every 3 seconds to handle late-connecting CLI instances
Permissions
Section titled “Permissions”iOS simulator permissions are managed via xcrun simctl privacy:
allow permission "camera" # simctl privacy grant <UDID> camera <bundleID>deny permission "location" # simctl privacy revoke <UDID> location <bundleID>grant all permissionsrevoke all permissionsThis works on iOS 14+ simulators for camera, location, microphone, photos, contacts, calendar and sms.
probe test --grant camera,location applies the same grants once before the first test.
Notifications have no simctl service, so they cannot be granted by name. Since 0.16.0 probe answers the system
alert instead: allow permission "notifications" and probe test --grant notifications tap Allow when the alert
appears, through the iOS system-dialog driver (an XCUITest runner; needs Xcode, simulators only). See
System dialogs. Android is unaffected: POST_NOTIFICATIONS is granted with
adb shell pm grant.
App language
Section titled “App language”set language "de" # relaunches the simulator app with -AppleLanguages (de) -AppleLocale deThe language is passed as launch arguments on every later launch of the app (including restart the app and clear app data), which
Flutter’s locale follows. System dialogs (permissions, share sheet) keep the simulator’s language: use the role-based
tap "Don't Allow" in system dialog for those. Not supported on physical devices (change the language in Settings > Apps).
probe test --locale de and --locales de,ja,ar apply it per run.
Device Media
Section titled “Device Media”add media "fixtures/photo.jpg" # xcrun simctl addmediaThe file lands in the simulator’s Photos library and is immediately visible to image pickers.
Simulators only — there is no devicectl equivalent for physical devices; the step skips with a
warning there.
Deep Links
Section titled “Deep Links”open link "myapp://profile/42" in the appDispatches xcrun simctl openurl, so a custom scheme or Universal Link registered by your app is
delivered to the app itself. Plain open link "https://..." still opens Safari via
url_launcher.
Native UI Automation
Section titled “Native UI Automation”The tap native / see native / type native verbs are Android-only today — iOS has no
uiautomator equivalent, and simctl cannot inspect or drive arbitrary UI. On iOS,
tap native/type native fail with a clear error rather than silently no-op’ing, and
see native reports “not found”. iOS support via WebDriverAgent is a written, scoped proposal
(docs/proposals/n2-ios-native-ui-bridging.md in the repo), not yet implemented.
Biometric Authentication (Face ID / Touch ID)
Section titled “Biometric Authentication (Face ID / Touch ID)”enroll biometric, biometric match, and biometric no match drive Face ID and Touch ID flows on iOS Simulator via xcrun simctl spawn booted notifyutil.
before all tests enroll biometric
test "Face ID unlocks the app" open the app tap "Sign in with Face ID" wait until "Sign in with Face ID" appears tap "Sign in with Face ID" biometric match wait until "Dashboard" appears see "Dashboard"
test "failed Face ID shows error" open the app tap "Sign in with Face ID" wait until "Sign in with Face ID" appears tap "Sign in with Face ID" biometric no match wait until "Authentication failed" appears see "Authentication failed"Multiple simulators: If more than one simulator is booted, the biometric notifyutil fires on booted (which selects an arbitrary device). Always boot only the target simulator, or pass --device <UDID> so the CLI targets the right one.
Video Recording
Section titled “Video Recording”iOS simulator video is recorded via xcrun simctl io <UDID> recordVideo. Videos use the h264 codec (not HEVC) for browser compatibility in HTML reports.
probe test tests/ --videoConfiguration
Section titled “Configuration”device: simulator_boot_timeout: 60s boot_poll_interval: 2s token_file_retries: 5 restart_delay: 500ms ios_device_id: A1B2C3D4-E5F6-7890-ABCD-EF1234567890 # pin to a simulator UDID (skips auto-detection)
agent: port: 48686 dial_timeout: 30s token_read_timeout: 30sSet ios_device_id to avoid passing --device <UDID> on every run. The --device flag takes precedence when provided.
Troubleshooting
Section titled “Troubleshooting”Token not found
Section titled “Token not found”If the CLI times out waiting for the auth token:
- Verify the app is running:
xcrun simctl list devices booted - Check the token file:
cat ~/Library/Developer/CoreSimulator/Devices/<UDID>/data/tmp/probe/token - Check logs manually:
xcrun simctl spawn <UDID> log stream --predicate 'eventMessage CONTAINS "PROBE_TOKEN="'
Build fails with SDK mismatch
Section titled “Build fails with SDK mismatch”When your Xcode SDK version differs from the target simulator iOS version, use xcodebuild with explicit -destination instead of flutter build.
Notification permission dialog blocks tests
Section titled “Notification permission dialog blocks tests”If a native notification permission dialog appears and blocks the Flutter UI, your app is requesting notification permissions without checking for PROBE_AGENT. See the Permissions section above for the fix.
Data clearing on iOS
Section titled “Data clearing on iOS”clear app data deletes the container subdirectories (Documents, Library, tmp) rather than uninstalling the app. Path validation prevents accidental deletion of non-container paths.