Creating an iOS Driver — Simulator and Real Device Sessions

6 min read

The iOS driver uses XCUITestOptions and targets either a simulator or a real device. The setup is more involved than Android — code signing for real devices, WebDriverAgent compilation, and simulator boot times add complexity, but the resulting driver is fast and accurate.

Simulator session

from appium import webdriver
from appium.options import XCUITestOptions
import os
 
def create_ios_driver() -> webdriver.Remote:
    options = XCUITestOptions()
    options.device_name = os.getenv("IOS_DEVICE", "iPhone 15")
    options.platform_version = os.getenv("IOS_VERSION", "17.0")
    options.app = os.path.abspath("apps/MyApp.app")  # unzipped .app bundle
    options.no_reset = True
    options.simulator_startup_timeout = 120_000  # 120 seconds in ms
    options.wda_startup_retries = 3
 
    return webdriver.Remote("http://127.0.0.1:4723", options=options)

Real device session

Real device testing requires code signing and a connected device with Developer Mode enabled:

def create_ios_real_device_driver() -> webdriver.Remote:
    options = XCUITestOptions()
    options.device_name = "My iPhone"  # name from Xcode
    options.udid = os.getenv("IOS_DEVICE_UDID")  # from `xcrun xctrace list devices`
    options.platform_version = "17.2"
    options.app = os.path.abspath("apps/MyApp.ipa")
    options.xcode_org_id = os.getenv("XCODE_ORG_ID")       # 10-char team ID
    options.xcode_signing_id = "Apple Development"
 
    return webdriver.Remote("http://127.0.0.1:4723", options=options)

Keeping the simulator warm between sessions

Booting a simulator from scratch takes 30–90 seconds. Set keep_alive = True to leave it running after the session ends:

options.keep_alive = True

The next session targeting the same simulator reuses the running instance. This saves around a minute per test file when using module-scoped fixtures.

Parallel iOS sessions

Each parallel iOS session needs its own WebDriverAgent port to avoid conflicts:

# Session 1 (iPhone 15)
options_iphone = XCUITestOptions()
options_iphone.device_name = "iPhone 15"
options_iphone.wda_local_port = 8100
 
# Session 2 (iPhone 14)
options_ipad = XCUITestOptions()
options_ipad.device_name = "iPhone 14"
options_ipad.wda_local_port = 8101

App lifecycle

# Activate by bundle ID
driver.activate_app("com.example.myapp")
 
# Get active bundle ID
bundle_id = driver.bundle_id
 
# Terminate
driver.terminate_app("com.example.myapp")
 
# Query installed
installed = driver.is_app_installed("com.example.myapp")

Clipboard (simulator only)

Clipboard access on real devices requires additional permissions. On simulators:

driver.set_clipboard_text("test@example.com")
email = driver.get_clipboard_text()

Handling system alerts

from selenium.common.exceptions import NoAlertPresentException, TimeoutException
from selenium.webdriver.support.ui import WebDriverWait
 
def accept_alert_if_present(driver, timeout=3):
    try:
        WebDriverWait(driver, timeout).until(
            lambda d: _try_accept_alert(d)
        )
    except TimeoutException:
        pass  # No alert shown
 
def _try_accept_alert(driver):
    try:
        driver.switch_to.alert.accept()
        return True
    except NoAlertPresentException:
        return False

Auto-accept all alerts via capability (use only when alert handling isn't under test):

options.auto_accept_alerts = True

Debugging session failures

WDA startup timeout: The most common iOS failure. Increase retries and wait:

options.wda_startup_retries = 5
options.wda_startup_retry_interval = 20_000  # ms between retries

Simulator not found: List available simulators:

xcrun simctl list devices available

The device_name must match exactly, including any "Pro" or "Max" suffixes.

Session hangs indefinitely: Set new_command_timeout to force-kill a stuck session:

options.new_command_timeout = 60  # seconds

If no command is sent within 60 seconds, Appium kills the session and frees the simulator.

Typical conftest.py fixture

@pytest.fixture(scope="function")
def ios_driver():
    options = XCUITestOptions()
    options.device_name = os.getenv("IOS_DEVICE", "iPhone 15")
    options.app = os.path.abspath("apps/MyApp.app")
    options.no_reset = True
    options.wda_startup_retries = 3
 
    driver = webdriver.Remote("http://127.0.0.1:4723", options=options)
    yield driver
 
    try:
        driver.quit()
    except Exception:
        pass

// tip to track lessons you complete and pick up where you left off across devices.