simctl
At the heart of Apple’s creation myth is the story of Steve Jobs’ visit to Xerox PARC in 1979. So inspired by the prototype for the Alto computer was he, that — like Prometheus — Steve Jobs would steal fire from the gods and bring it to the masses by way of the Lisa and Macintosh computers a few years later.
Like so many creation myths, this one is based on dubious historical authenticity. But this is the story we like to tell ourselves because the mouse and graphical user interfaces were indeed revolutionary. With a mouse, anyone can use a computer, even if they aren’t a “computer person”.
…though, if you are a “computer person”, you probably use keyboard shortcuts for your most frequent operations, like saving with ⌘S instead of File > Save. More adventurous folks may even venture into the Utilities folder and bravely copy-paste commands into Terminal for one-off tasks like bulk renaming. This is how many programmers get their start; once you see the power of automation, it’s hard not to extend that to do more and more.
Which brings us to today’s subject:
If you make apps,
you spend a good chunk of your time in Xcode
with Simulator running in the background.
After exploring the basic functionality of Simulator,
you might feel compelled to automate the more time-consuming tasks.
The simctl
command-line tool provides you the interface you need
to use and configure Simulator in a programmatic way.
If you have Xcode installed,
simctl
is accessible through the xcrun
command
(which routes binary executables to the active copy of Xcode on your system).
You can get a description of the utility
and a complete list of subcommands by running simctl
without any arguments:
$ xcrun simctl
Many of these subcommands are self-explanatory
and offer a command-line interface to functionality
accessible through the Simulator app itself.
However, the best way to understand how they all work together
is to start with the list
subcommand.
Managing Simulator Devices
Run the list
subcommand to get a list of the available
runtimes, device types, devices, and device pairs:
$ xcrun simctl list
-- i OS 12.1 --
i Phone 5s (CC96B643-067E-41D5-B497-1AFD7B3D0A13) (Shutdown)
i Phone 6 (7A27C0B9-411A-4BCD-8A67-68F00DF39C1A) (Shutdown)
i Phone 6 Plus (A5918644-8D46-4C67-B21E-68EA25997F91) (Shutdown)
i Phone 6s (1AB5A4EB-2434-42E4-9D2C-42E479CE8BDC) (Shutdown)
…
Each device has an associated
UDID.
You pass this to any of the simctl
subcommands that take a device parameter.
One such subcommand is boot
,
which starts up the specified device,
making it available for interaction:
$ xcrun simctl boot $UDID
To see the booted simulator in action,
open the Simulator app with the open
command, like so:
$ open /Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/
You can see which devices are booted
by redirecting the output of list
to a grep
search for the term “Booted”:
$ xcrun simctl list | grep Booted
i Phone X (9FED67A2-3D0A-4C9C-88AC-28A9CCA44C60) (Booted)
To isolate the device identifier,
you can redirect to grep
again to search for a UDID pattern:
$ xcrun simctl list devices | \
grep "(Booted)" | \
grep -E -o -i "([0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})"
9FED67A2-3D0A-4C9C-88AC-28A9CCA44C60
Pass the --json
or -j
option to list
and other subcommands
to format output as JSON.
Having a structured representation of resources like this is convenient
when you’re writing scripts or programs for managing simulators.
$ xcrun simctl list --json
{
"devicetypes" : [
…
{
"name" : "i Phone X",
"bundle Path" : "\/Applications\/Xcode-beta.app\/Contents\/Developer\/Platforms\/i Phone OS.platform\/Developer\/Library\/Core Simulator\/Profiles\/Device Types\/i Phone X.simdevicetype",
"identifier" : "com.apple.Core Simulator.Sim Device Type.i Phone-X"
},
…
]
}
When you’re finished with a device,
you can shut down and erase its contents
using the shutdown
and erase
subcommands:
$ xcrun simctl shutdown $UDID
$ xcrun simctl erase $UDID
Copying & Pasting Between Desktop and Simulator
It’s easy to feel “trapped” when using Simulator,
unable to interact directly with the OS as you would any other app.
Recent versions of Simulator have improved this
by allowing you to automatically synchronize pasteboards
between desktop and simulated device.
But sometimes it’s more convenient to do this programmatically,
which is when you’ll want to break out the pbcopy
and pbpaste
subcommands.
Be aware that the semantics of these subcommands might be opposite of what you’d expect:
-
pbpaste
takes the pasteboard contents of the Simulator and copies them to the desktop’s main pasteboard -
pbcopy
takes the main pasteboard contents from your desktop and copies them to the pasteboard of the device in the Simulator
For example,
if you wanted to copy the contents of a file on my Desktop
to the pasteboard of a simulated device,
you’d use the pbpaste
subcommand like so:
$ cat ~/Desktop/file.txt | pbcopy
$ xcrun simctl pbpaste booted
After running these commands,
the contents of file.txt
will be used
the next time you paste in Simulator.
Opening URLs in Simulator
You can use the openurl
subcommand
to have Simulator open up the specified URL
on a particular device.
This URL may be a webpage, like this one:
$ xcrun simctl openurl booted "https://nshipster.com/simctl"
…or it can be a custom scheme associated with an app,
such as maps://
for Apple Maps:
$ xcrun simctl openurl booted maps://?s=Apple+Park
Add to Photo Stream
We’ve all been there before: developing a profile photo uploader and becoming all-too-familiar with the handful of photos of flora from San Francisco and Iceland that come pre-loaded on Simulator.
Fortunately, you can mix things up
by using the addmedia
subcommand
to add a photo or movie to the Photos library of the specified simulator:
$ xcrun simctl addmedia booted ~/Desktop/mindblown.gif
Alternatively, you can drag and drop files from Finder into the Photos app in Simulator to achieve the same effect.
Capture Video Recording from Simulator
Sure, you can use the global macOS shortcut
to capture a screenshot of a simulator
(⇧⌘4),
but if you intend to use those for your App Store listing,
you’re much better off using the io
subcommand:
$ xcrun simctl io booted screenshot app-screenshot.png
You can also use this subcommand to capture a video as you interact with your app from the simulator (or don’t, in the case of automated UI tests):
$ xcrun simctl io booted record Video app-preview.mp4
Setting Simulator Locale
One of the most arduous tasks for developers localizing their app is switching back and forth between different locales. This process typically involves manually tapping into the Settings app, navigating to General > Language & Region selecting a region from a long modal list and then waiting for the device to restart. Each time takes about a minute — or maybe longer if you’re switching back from an unfamiliar locale (你懂中文吗?)
But knowing how simulators work in Xcode lets you automate this process
in ways that don’t directly involve simctl
.
For example,
now that you know that each simulator device has a UDID,
you can now find the data directory for a device in ~/Library/Developer/Core
.
The simulator preferences file
data/Library/Preferences/.Global
contains a number of global settings
including current locale and languages.
You can see the contents of this using the plutil
command:
$ plutil -p ~/Library/Developer/Core Simulator/Devices/$UDID/data/Library/Preferences/.Global Preferences.plist
Here’s what you might expect, in JSON format:
{
"Adding Emoji Keybord Handled": true,
"Apple Languages": ["en"],
"Apple Locale": "en_US",
"Apple ITunes Store Item Kinds": [ … ],
"Apple Keyboards": [
"en_US@sw=QWERTY;hw=Automatic",
"emoji@sw=Emoji",
"en_US@sw=QWERTY;hw=Automatic"
],
"Apple Keyboards Expanded": 1,
"Apple Passcode Keyboards": ["en_US", "emoji"],
"AKLast IDMSEnvironment": 0,
"PKKeychain Version Key": 4
}
You can use the plutil
command again
to modify the preferences programmatically.
For example, the following shell script
changes the current locale and language to Japanese:
PLIST=~/Library/Developer/Core Simulator/Devices/$UDID/data/Library/Preferences/.Global Preferences.plist
LANGUAGE="ja"
LOCALE="ja_JP"
plutil -replace Apple Locale -string $LOCALE $PLIST
plutil -replace Apple Languages -json "[ \"$LANGUAGE\" ]" $PLIST
You can use this same technique to adjust Accessibility settings
(com.apple.Accessibility.plist
),
such as enabling or disabling Voice Over and other assistive technologies.
To get the full scope of all the available settings,
run plutil -p
on all property lists in the preferences directory:
$ plutil -p ~/Library/Developer/Core Simulator/Devices/$UDID/data/Library/Preferences/*.plist
At least some share of the credit for the popularity and success of iOS as a development platform can be given to the power and convenience provided by Simulator.
To be able to test your app on dozens of different devices and operating systems is something that we might take for granted, but is an order of magnitude better than the experience on other platforms. And the further ability to script and potentially automate these interactions makes for an even better developer experience.