Code Once, Build Once, Launch All!
How to develop and automate your app for all devices at once.
Note: The following article is compiled based on the subject material of two internal “Lightning Talk” presentations made to the iOS guild at carsales.
Introduction:
The Retail iOS team at carsales is responsible for work on the consumer-facing iOS apps for all of the various marketplaces hosted by carsales.
There are 4 live domestic apps for the Australian Market:
- Carsales
- Bikesales
- Boatsales
- CaravanCampingSales
There are also 3 live apps for the Latin-American market:
- Chileautos
- Demotores
- Soloautos
All of these apps are available on the full range of iOS devices, which cover a number of screen sizes and layouts.
The iOS apps are a prominent front-end for our platforms. Users can use the various websites on desktop to browse and enquire on sales listings, but apps allow for a very engaging, instantaneous and “in-the-pocket” experience when participating in the market. Obviously, a key focus for our team is to make that experience as intuitive and enjoyable as possible, across all device types.
The Problem
When developing for mobile devices, we need to be able to cater for all of the various devices that could be used. This ranges from the smaller phones such as the iPhone SE, to the new “X” series of iPhones (iPhone X, iPhone XR and iPhone XS/XS-Max), and on the extreme end of display sizes we have the iPad Pro range.
When developing a new feature, we work closely with the internal design teams to determine the best way to present the interface for users, so that it’s clear and intuitive, and consistent with the expected UX for that platform. One thing to consider is that the same feature may need to be presented differently on an iPhone device, as opposed to an iPad device. A good example of this is viewing the sales listings, as shown below.
When developing these features, we need to look at all of the designs first, in order to develop the architecture in the best way to present it on all device types. After all, it’s the same application code running on all device types.
This means that the role of the Retail iOS team members requires us to be able to implement technical features, present them (in discussion with our designers) in a consistent way across all iOS device types, and most importantly, be able to do this all at speed, given the constant rate of work that the team handles.
The Crux: Manual Sequential Testing, for Each Device
The key issue here is the length of time required to test on all devices manually. We have access to Simulators for all devices, but usually in order to test for a number of devices, after we’ve finished writing the code we need to manually:
- Select a simulator type in Xcode
- Build/Run, and wait for the app to compile and install on that simulator
- Test the feature on that simulator
- Once we’re happy that everything is as it should be for that device, we can stop the build in Xcode, then select the next Simulator device type and then Build/Run for each (rinse and repeat)
As you can imagine, this can become a time-intensive process, especially when we want to cover a range of device types. Also, if it’s a manual process, it may become overly laborious to do so, and hence testing like this may be discouraged or forgotten.
What if we could build and test to all devices simultaneously?
How Can We Solve This?
Xcode is the primary tool in iOS and macOS software development. It’s an IDE provided by Apple, and comes with iOS Simulators so you can write your code in Xcode, then build and run in a simulated device, displayed on your screen. Simulators allow quick iteration during development, and also allow you to test your app in devices you don’t have physical access to (however it is of course important to build and test on actual devices).
Xcode is a graphical user interface, so there are lots of tools in the UI to help developers as they implement their features. However, there is also a suite of command-line tools that can also be utilised in the build process. This means that (among other things) developers can add additional scripts to the pre and post-build phases, which can automate tests and other operations.
We can use scripts such as these to speed up our development, and test on multiple device types simultaneously.
The following section outlines a number of included tools that developers can use to manage iOS Simulators.
Command-line Simulator Control:
xcrun provides a means to locate or invoke developer tools from the command line.
xcrun simctl provides a command-line interface to the “iOS Simulator” GUI application. This application can be launched manually via the Applications Folder, and simulators can be created/deleted via the “Devices and Simulators” window in Xcode. The commands below give direct access to these and additional functionalities.
You can create simulators at any time, for any of the available device types and runtimes. These can be in addition (and separate) to the default Xcode-provided simulators. To see a list of the installed devices (likely a long list, broken down by OS versions, and device types, and will include iOS, TVOS and WatchOS devices) use the following command in Terminal:
xcrun simctl list
You can filter this list to display just “Booted” devices, or for a specific device type, for example:
xcrun simctl list | grep Booted
simctl can operate with any of the devices or runtimes installed on your mac. This varies, depending on which version(s) of Xcode you have installed. Note also that this will be affected if you have multiple versions of Xcode installed, and which one is active (for example if you use a tool such as xcversion, and have flashy new Beta versions of Xcode 11 installed).
xcrun simctl list devicetypesxcrun simctl list runtimes
Create Simulators
To create a new simulator, you must specify a name for the simulator, as well as a device type and runtime (both from the lists via the commands directly above)
xcrun simctl create <simulatorName> <devicetype> <runtime>xcrun simctl create NewiPhoneX com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-12-0
The second command above creates a new “iPhone X” simulator, named “NewiPhoneX”, running iOS 12.0. This command will return a UDID which can be used in other commands as outlined below.
Once a simulator has been created, it will be immedately available in the Xcode GUI.
Boot Simulator
You can boot any simulator by providing the UDID for it.
xcrun simctl boot <UDID>
This won’t launch the “Simulators” app, but if it is already open, you will see the new simulator window appear as the device boots.
Note: If one simulator is booted at the time, some of the below commands can use the string “booted” instead of needing to specify the UDID — this will only work for a single booted simulator, not all booted simulators at that time.
Shutdown Simulator
You can shut down any simulator by providing the UDID, or specifying “all”.
xcrun simctl shutdown <UDID>xcrun simctl shutdown all
Erase & Delete Simulators
Erasing a simulator performs a “Factory Reset”.
xcrun simctl erase <UDID>xcrun simctl erase all
Deleting a simulator actually removes the simulator from your system, deleting all apps and data.
xcrun simctl delete <UDID>
At any point in the future, you can recreate simulators, as the device types and runtimes will still be available on your system, even if you delete all simulators from your machine.
If you prefer to run a “Lean and Mean” development environment, you can delete all of the default Simulators provided by Xcode, and then install a custom list of devices.
Handy tip: If you’ve updated to a new release of Xcode, and don’t need to manage multiple Xcode versions for backwards compatibility, some of your older iOS Simulators may become obsolete (and hence unavailable) for the new version of Xcode. macOS will not automatically delete those obsolete simulators, but the following command can be used:
xcrun simctl delete unavailable
Add Media
If you have photos/videos on your mac that you want to be accessible on an iOS Simulator, you can transfer them to a simulator with the following:
xcrun simctl addmedia <UDID> <pathToImages>
After this, the media will be present in that simulator’s “Photos” app. Please note that the Photo library is not shared between simulators.
Launch Apps
You can directly launch or terminate an app by specifying the bundle identifier. Note that the app must be installed prior to these commands.
xcrun simctl launch booted com.company.app
xcrun simctl terminate booted com.company.app
Custom Localisation Settings
For localisation testing (for example for the carsales Latin-American apps), we can specify custom settings when launching an app.
xcrun simctl launch booted com.company.app -AppleLanguages "(es-MX)"
You can specify, among others: AppleLanguages, AppleKeyboards, and AppleLocale.
View Application Folders
If you need to browse various data folders for simulators on your machine, you can request those folders by specifying the UDID, the folder type, and the bundle identifier if applicable.
To view the directory structure for the entire simulator:
xcrun simctl getenv <UDID> SIMULATOR_SHARED_RESOURCES_DIRECTORYopen `xcrun simctl getenv <UDID> SIMULATOR_SHARED_RESOURCES_DIRECTORY`
To open various container folders for a specific app:
xcrun simctl get_app_container booted <bundleID> <container>open `xcrun simctl get_app_container booted com.company.app data`
Where <container> can be:
- `app` — the .app bundle (default)
- `data` — the application’s data container
- `groups` — The App Group containers
- (group identifier) — A specific App Group container
The above section summarised some of the command line tools available to iOS developers for creating and managing iOS Simulators.
The following section outlines some ways that developers can combine these tools, and automate Simulator operations to install and launch apps.
Let’s tie all of this together now
You can see from the previous section that there are lots of tools available to developers, to be able to customise their simulators to improve their workflow. Having command-line access to these tools also allows us to automate these steps. We can create scripts that use these commands, and we can integrate them into the various build-phases in your Xcode project so they are automatically executed as needed.
The key outcome for this article is to outline an automation process we use at carsales, so we can simultaneously build and run our app on many simulators. A process similar to this can be used to automate other workflows
Multi-Simulator Launch — The Process
To address the situation outlined in the introduction, there will be some times when we want to launch our app in not one but multiple iOS Simulators, so we can observe and test multiple devices simultaneously. Note that multiple simulators will be running and interactive, but the Xcode debugger will only be connected to the “Multi-Size Simulator” described below.
The process we will use for this workflow is:
- Configure Xcode to automatically create a new iOS Simulator called “Multi-Size Simulators”, if it does not already exist. The key aspect of the simulator is its unique name (you can change this name if you wish).
- Specify a list of device types that we want to run simultaneously.
- Create a bash script that will boot and launch the app in all of the listed simulators.
- Configure your Xcode project, so that if the “Multi-Size Simulators” Sim is selected for the Run process, it will run the bash script in the previous step.
Pre-build step
In Xcode, select your target, and then “Edit Scheme”. You want to select “Build”, then “Pre-actions” to add a script object to execute prior to building the app.
custom_sim=`xcrun simctl list | grep 'Multi-Size Simulators' | awk -F'[()]' '{print $2}'`if [ -z "${custom_sim}" ]; thenxcrun simctl create Multi-Size\ Simulators com.apple.CoreSimulator.SimDeviceType.iPhone-X `xcrun simctl list runtimes | grep iOS | tail -1 | sed -e 's/^.*) - //p' | sort -u`fi
This script does the following:
- Fetches the list of all simulators, filtered based on our specified unique name “Multi-Size Simulators”, and fetches the UDID for that simulator. This UDID is stored as “custom_sim”.
- If “custom_sim” is not defined (i.e. there is no simulator with that name), then create a simulator with that name, of type iPhone X, using the most recent runtime available.
Note: You can change the name of your “Multi-Size Simulators” device here if you like. The simulator will only be created once, so you may need to cater for a future situation where your simulator becomes obsolete from a future update of Xcode (the simulator will be there but inaccessible for execution).
Build a Simulator List
For this we just need a text file, with one Simulator per line. Each line will include a device name, and an optional iOS version. Save this file in your project folder as “MultiSimList.txt” or similar. An example file could include:
MyiPhoneSE (12.1)
iPad (5th generation)
Please note: Even though there are only two devices listed here, the simulator named “Multi-Size Simulators” will also boot and run your app (in this case our third Simulator will be an iPhone X).
Note each name includes the descriptive name of the device. This is the name as it is displayed when selecting a simulator in the drop-down list in Xcode. This allows this process to be shared between developers on separate machines. We can’t specify UDID’s here as they will be different for each computer, plus descriptive names are much easier to read.
You can specify an iOS version for each, or leave it blank. If you specify an iOS version, ensure that there is a simulator created on your machine for that device type and runtime.
Note: This can also be used to create a “Multi-iOS” list, for example to run your app on the same device type, but on multiple versions of iOS, in order to test backwards compatibility for new features.
Multi-Launch Script
Use your preferred editor-of-choice, and create a new bash script with the following contents (modify the app name, path and bundle identifier as needed):
#!/bin/bashxcrun simctl shutdown allpath=$(find ~/Library/Developer/Xcode/DerivedData/Carsales-*/Build/Products/Debug-iphonesimulator -name "App_Name.app" | tail -n 1)filename=./MultiSimList.txtgrep -v '^#' $filename | while read -r linedoxcrun instruments -w "$line"xcrun simctl install booted $pathxcrun simctl launch booted com.company.appdone
Save this script as launch_multiple_simulators.sh in your project folder.
This script performs the following actions:
- Shuts down all simulators;
- Find the path to the freshly-built binary in the Xcode build folder; and
- Iterate through the “MultiSimList.txt” file
For each simulator in that list:
- Launch that simulator;
- Install the binary to that simulator; and
- Launch the app via the bundle identifier
Note: We use xcrun instruments to boot the simulator instead of simctl so that we can use the descriptive name of the simulator, instead of needing to specify the UDID. Once that simulator is booted, we can use the booted parameter in the subsequent commands.
Post-Run Phase
The last thing we need is to add a script step to the Build phase in your app target.
In the definition of that script object, enter the following command:
custom_sim=`xcrun simctl list | grep 'Multi-Size Simulators' | awk -F'[()]' '{print $2}'`if [ ! -z "${custom_sim}" ] && [ "${TARGET_DEVICE_IDENTIFIER}" = "${custom_sim}" ]; then/bin/sh ${PROJECT_DIR}/launch_multiple_simulators.shfi
This script is similar in structure to the pre-build script, and performs the following actions:
- Fetches the list of all simulators, filtered based on our specified unique name “Multi-Size Simulators”, and fetches the UDID for that simulator. This UDID is stored as “custom_sim”.
- If “custom_sim” IS defined, and is also the current build target set in Xcode, then execute the bash script created in the previous step (modify the path in the last line as needed).
The Results:
Using the configuration outlined above, we can develop and test in a single simulator for the most part, following our usual development workflow. When we are at a point where we wish to verify a feature or UI in multiple device types, all we need to do is select our “Multi-Size Simulators” device in Xcode, select “Build and Run”, and Xcode will follow our instructions to install and launch our app in all of those simulators (be prepared for a little extra fan noise at this point!).
This addresses the crux of our problem, which was the time-intensive and manual process for building and launching each simulator type independently and in sequence. Now, we can have all simulators launch at the same time, and we can even test them all simultaneously, and directly compare the results in each, side by side.
Where Else From Here?
In this article we’ve outlined a number of tools that can be used to improve your workflow, and how some of these tools can be automated to perform specific tasks in your workflow.
You can use these tools in different configurations to add further benefits, such as:
- Create a brand new simulator of a specific device and runtime, and have it pre-loaded with a collection of photos to test image uploading in an app.
- Clean out the bulk of the default Xcode-provided simulators, and create a smaller list of Sims, for a lightweight development environment.
- Set up scripts to run your app in multiple simulators, covering all of your supported iOS versions (rather than multiple device sizes). This can be used for backwards compatibility checks.
- …Profit?
Conclusion
This article outlines a number of the developer tools that can be used by iOS developers to improve their workflow while developing iOS apps. It outlines a process that allows developers to be able to develop a feature, and then choose to launch it in multiple iOS simulators at once, to confirm that a new feature works and is presented intuitively for all devices.
At this time I’d like to thank the whole of the Retail iOS team at carsales, for being such a supportive and engaging team to work with. I’d also like to thank the Team Lead, Ben Thomas, for kicking off the Lightning Talks initiative within the iOS Guild at carsales. It’s been a wonderful opportunity for all of the developers here to not only learn from others, but also to inspire us to learn and present new skills.
At carsales we are always looking for fun and talented people to work with. If you liked what you read and are interesting in joining, please check out what positions we have available in our careers section.