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
-- iOS 12.1 --
    iPhone 5s (CC96B643-067E-41D5-B497-1AFD7B3D0A13) (Shutdown)
    iPhone 6 (7A27C0B9-411A-4BCD-8A67-68F00DF39C1A) (Shutdown)
    iPhone 6 Plus (A5918644-8D46-4C67-B21E-68EA25997F91) (Shutdown)
    iPhone 6s (1AB5A4EB-2434-42E4-9D2C-42E479CE8BDC) (Shutdown)
…

Each device has an associated UUID. 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 $UUID

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
iPhone X (9FED67A2-3D0A-4C9C-88AC-28A9CCA44C60) (Booted)

To isolate the device identifier, you can redirect to grep again to search for a UUID 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" : "iPhone X",
      "bundlePath" : "\/Applications\/Xcode-beta.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/DeviceTypes\/iPhone X.simdevicetype",
      "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-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 $UUID
$ xcrun simctl erase $UUID

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:

$ pbcopy ~/Desktop/file.txt
$ 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 recordVideo 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 UUID, you can now find the data directory for a device in ~/Library/Developer/CoreSimulator/Devices/. The simulator preferences file data/Library/Preferences/.GlobalPreferences.plist 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/CoreSimulator/Devices/$UUID/data/Library/Preferences/.GlobalPreferences.plist

Here’s what you might expect, in JSON format:

{
  "AddingEmojiKeybordHandled": true,
  "AppleLanguages": ["en"],
  "AppleLocale": "en_US",
  "AppleITunesStoreItemKinds": [  ],
  "AppleKeyboards": [
    "en_US@sw=QWERTY;hw=Automatic",
    "emoji@sw=Emoji",
    "en_US@sw=QWERTY;hw=Automatic"
  ],
  "AppleKeyboardsExpanded": 1,
  "ApplePasscodeKeyboards": ["en_US", "emoji"],
  "AKLastIDMSEnvironment": 0,
  "PKKeychainVersionKey": 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/CoreSimulator/Devices/$UUID/data/Library/Preferences/.GlobalPreferences.plist

LANGUAGE="ja"
LOCALE="ja_JP"

plutil -replace AppleLocale -string $LOCALE $PLIST
plutil -replace AppleLanguages -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/CoreSimulator/Devices/$UUID/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.