Controlling an iPhone With Python/Appium on A Macbook (0 to 1)

Kevin Lin
8 min readAug 25, 2024

--

While working on a personal project, I had a fairly straightforward set of requirements:

  • Control my real, physical iPhone using Appium
  • Control it wirelessly from my Macbook
  • Do it for free

I couldn’t find a guide that didn’t skip over steps or make assumptions about what I had installed/what I knew, so here’s my best attempt at a comprehensive guide from start to finish. I address all the roadblocks I ran into, but I am no expert, so I can’t predict all the other errors you might run into. Nevertheless, I ran into so many issues that I’m sure someone will find this helpful.

Prerequisites: a Macbook, iPhone, and a cable to connect them to start.

On your iPhone:

  1. Connect to your Macbook using the cable, and trust the Macbook device when prompted on the phone.

2. Open Settings -> Privacy and Security -> scroll down and turn on Developer mode. You’ll be asked to restart your phone.

3. Go to Settings -> Developer -> turn on “Enable UI Automation”

On your Macbook:

  1. Install Xcode. Make sure the Xcode version is compatible with your iOS version. I used the most recent version of both.
  2. Install Xcode command line tools in your terminal with
xcode-select --install

3. Run Xcode as an app and install the latest iOS SDK when prompted

4. Install Homebrew on your terminal

5. Install npm using Homebrew

brew install node

6. Install appium using npm

npm i -g appium

7. Install the xcuitest driver using appium:

appium driver install xcuitest

8. Run appium in a terminal with just appium :

9. Install Appium Inspector from Github (download the one that says -universal-macos, unzip). Running it might give a “can’t be opened because Apple cannot check it for malicious software” error, go to System Preferences -> Privacy and Security, look for the Appium Inspector notification and click “Open anyway”.

10. We need to get the UDID of the iPhone. In another terminal, run xcrun xctrace list devices . If it doesn’t show your iPhone, try opening Xcode and opening a project, as this got my phone to prompt me to trust the computer again. If it says it can’t find the xctrace utility, this may be because you installed Xcode multiple times, or installed it once with Brew and then through the Mac store. This SO reply gave me the command to solve this: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer. Copy the ID next to your iPhone device from the list devices command and store it somewhere temporarily.

11. We also need an Apple Development certificate. This link somewhat explains it, but it wasn’t very intuitive. I opened Xcode, at the top bar, I clicked Xcode->Settings->Accounts. I added my Apple ID as an account (bottom left of below screenshot), and then I clicked Manage Certificates, and added Apple Development certificates.

Then, I opened “Keychain Access” on my Macbook, clicked “login” on the left, looked for “Apple Development: myemail@gmail.com (code)” and copied the 10 character code next to my Apple ID. Someone else’s screenshot:

12. Create a test configuration in Appium Inspector:

Under “Capability Builder” in Appium Inspector, add these Key value pairs either using the field entry boxes or the JSON editor:

{
"platformName": "iOS",
"appium:bundleId": "com.apple.Preferences",
"appium:automationName": "XCUITest",
"appium:udid": "<your udid from step 10",
"appium:xcodeSigningId": "iPhone Developer",
"appium:xcodeOrgId": "<your Apple Development ID from step 11"
}

Make sure you replace my placeholders with your values. This bundleId is the app that gets opened at the start of the test, and I’ve just put the Settings as an easy app to start with.

If you press “Start Session” in Appium Inspector’s bottom right, you’ll probably get an error, like “xcode build failed with code 70”. This is because the WebDriverAgent isn’t on the phone yet.

13. Build WebDriverAgent for the iPhone:

The Appium server/driver needs to communicate with a WebDriverAgentRunner app on your phone. I think the issue here is that our free Apple Developer account can’t sign the WebDriverAgentRunner app to add to the phone.

To fix this, as described here,

  • In Xcode, File->Open ~/.appium/node_modules/appium-xcuitest-driver/node_modules/appium-webdriveragent/WebDriverAgent.xcodeproj (you can use “Command + Shift + G” in Finder to type in a path). If Appium is installed somewhere else, you’ll have to find the path to it and the project.
  • Click on selection and choose WebDriverAgent:
  • At the top, select your real phone/Simulator you’d like to run automated tests on as the target, and the thing being built as WebDriverAgentRunner. If your phone is saying that it’s not connected, go to Window -> Devices and Simulators, and unplug and plug in your phone a few times, and wait for the things Xcode runs to finish.
  • In the main window, choose WebDriverAgentRunner as target, and go to the Signing & Capabilities tab:
  • On this screen, I had an error about signing/provisioning. This is because I am using a free Apple developer account.
  • I followed the instructions here to make a valid provisioning file and copied the new bundle ID which was <my- name>.WebDriverAgentRunner
  • I reopened the WebDriverAgent project and used my new bundle ID under “Signing & Capabilities”:
  • I set my target to be WebDriverAgentRunner and the device to be my Phone again:
  • In Xcode, I clicked from the menu Product -> Test. This made my phone tell me that my developer team was untrusted, so on my iPhone, I went into Settings -> General -> VPN & Devices Management, and trusted my own development team. Finally, this added WebDriverAgentRunner to my Phone.

14. Running the Appium Session

Back in Appium Inspector, I added “updatedWDABundleId” to my capabilities, with the value being “Kevin-Lin.WebDriverAgentRunner”. I pressed “Start Session” on Appium Inspector and then I had a “xcodebuild failed with code 65” error.

This medium article said that this was expected, but the solutions were things that we’ve already done. I added the Appium capability: "showXcodeLog": "true" and inspected the Appium server logs for hints, but didn’t see anything.

The solution (for some reason) was just to reload the WDARunner onto my phone again. I ran Product->Test on Xcode again to add the WebDriverAgentRunner to my phone, permitted my team’s apps to work in VPN & Devices Management again, and this time, clicking “Start Session” in Appium Inspector worked!

Appium Inspector showing my Phone’s Settings screen because the session started successfully

Sometimes, this would stop working after killing a session. In those cases, restarting appium in the command line, and reinstalling WebDriverAgentRunner onto the phone using Xcode usually solved the issue.

I think breaking past those roadblocks is what most people are interested in, but I also wanted to connect wirelessly and use Python.

Connecting Wirelessly

In Xcode, go to Window -> Devices and Simulators, and make sure your phone has “Connect via network” checked. If it’s not checked, you might need to plug in your device to check it.

Then, many online guides will say that you can unplug your phone, run idevice_id --list in your terminal, and still see your phone’s UDID. This was not the case for me, this command returned nothing. The solution was to plug my iPhone back into my Macbook, click on my iPhone on the left sidebar of Finder, and, after it loads, check “Show this iPhone when on Wi-fi”:

I clicked Apply in the bottom right, and then Sync. After doing this and unplugging my phone, my device still didn’t show up when I ran idevice_id --list , but when I ran my Appium Inspector test, it worked!

Python

For my project, I wanted to write code in Python to be able to control the test using my own custom logic. This part is just extra help for this guide, you may not need to do this, or you may want to use another language.

In Appium Inspector, you can copy the code to replicate what you just ran:

I already had Python installed, but if you don’t have it I recommend installing pyenv with Brew and then installing Python with pyenv.

I set up a virtual environment in a new project, ran pip install Appium-Python-Client<4 , and then wrote some simple custom code to control swipe left/right depending on what I typed into the terminal:

from appium import webdriver
from appium.options.common.base import AppiumOptions
from appium.webdriver.common.touch_action import TouchAction

# For W3C actions
from selenium.webdriver.common.action_chains import ActionChains
options = AppiumOptions()
options.load_capabilities({
"platformName": "iOS",
"appium:bundleId": "com.apple.Preferences",
"appium:automationName": "XCUITest",
"appium:udid": "<my-udid>",
"appium:xcodeSigningId": "iPhone Developer",
"appium:xcodeOrgId": "<my apple developer team id>",
"appium:updatedWDABundleId": "Kevin-Lin.WebDriverAgentRunner",
"appium:showXcodeLog": "true",
"appium:includeSafariInWebviews": True,
"appium:newCommandTimeout": 3600,
"appium:connectHardwareKeyboard": True
})
driver = webdriver.Remote("http://127.0.0.1:4723", options=options)
# set up swiping
deviceSize = driver.get_window_size()
print("Device Width and Height : ",deviceSize)
screenWidth = deviceSize['width']
screenHeight = deviceSize['height']
x_left = screenWidth/9
x_right = screenWidth*8/9
y_pos = screenHeight/2
endy2 = screenHeight/2
actions = TouchAction(driver)

def swipe_left():
actions = ActionChains(driver)
actions.w3c_actions.pointer_action.move_to_location(x_left, y_pos)
actions.w3c_actions.pointer_action.click_and_hold()
actions.w3c_actions.pointer_action.move_to_location(x_right, y_pos)
actions.perform()

def swipe_right():
actions = ActionChains(driver)
actions.w3c_actions.pointer_action.move_to_location(x_right, y_pos)
actions.w3c_actions.pointer_action.click_and_hold()
actions.w3c_actions.pointer_action.move_to_location(x_left, y_pos)
actions.perform()

while True:
# if input on terminal is "w", scroll up, if it's "s" scroll down
direction = input()
if direction == "d":
swipe_right()
elif direction == "a":
swipe_left()
else:
break
driver.quit()

With that, I had a nice skeleton to build my project off of. I hope this guide was helpful; whenever I encounter a hard task where the path is unclear, I like to document the progress to understand it later, with the added benefit of helping others. If you run into any issues and find their solution, please let me know so I can add it to this guide.

Thanks for reading!

--

--

Kevin Lin

Engineering Physicist and occasional content creator