GitHub Actions are powerful tools and a great complement to Xcode Cloud for Swift projects’ continuous integration.

However, I encountered an issue: when using xcodebuild test for iOS, a concrete device is required:

Cannot test target “FooTests” on “Any iOS Device”: Tests must be run on a concrete device

The challenge then arose: how to identify the simulator identifiers suitable for the intended device?

Of course, Apple provides a command for this:

xcrun simctl list devices available

But then CI requires parsing and filtering on this command’s output. Here are 3 solutions to do so.

Fastlane era

Fastlane provides a FastlaneCore::DeviceManager that greatly assists in inspecting available devices.

require 'fastlane_core/device_manager'

# Usage: latest_simulator_with_name('iPhone Xʀ').udid
def latest_simulator_with_name(device_name)
  result =
    FastlaneCore::DeviceManager.simulators
                               .select { |s| s.name == device_name }
                               .max_by { |s| Gem::Version.create(s.os_version) }
  return result unless result.nil?

  FastlaneCore::UI.error "#{device_name} missing. Please select one of the following:"
  FastlaneCore::DeviceManager.simulators
                             .each { |s| FastlaneCore::UI.message "#{s.name} (#{s.os_version})" }
  raise "Device with name #{device_name} cannot be found. Please install it."
end

Despite my extensive use of Fastlane, I’m starting to perceive a decline in its relevance with the emergence of Xcode Cloud.

Exploring the PointFreeCo Approach

As an admirer of the PointFreeCo team’s work, I investigated their methods and discovered a useful yet succinct shell command.

PLATFORM_IOS = iOS Simulator,id=$(call udid_for,iOS 17.2,iPhone \d\+ Pro [^M])



define udid_for
$(shell xcrun simctl list devices available '$(1)' | grep '$(2)' | sort -r | head -1 | awk -F '[()]' '{ print $$(NF-3) }')
endef

While efficient, it lacks readability.

Implementing a Swift Solution

To address this challenge, I developed a Swift script to obtain the necessary information. You can find the script on the Scripts section of my Blocks project or as a command of the CLI that I provide with this tool.

To use it in continuous integration (CI), follow these steps:

# Download the script and make it executable
curl -sSL https://raw.githubusercontent.com/dirtyhenry/swift-blocks/main/Scripts/ListDevices.swift \
  -o findDevice
chmod +x findDevice

# Find the device identifier
DEVICE_ID=$(./findDevice $TEST_IOS_VERSION $TEST_IOS_SIMULATOR_MODEL)

# Run the test
set -o pipefail && xcodebuild test \
    -skipMacroValidation -skipPackagePluginValidation \
    -workspace foo.xcworkspace \
    -scheme "bar" \
    -destination platform="iOS Simulator,id=$DEVICE_ID" \
    | xcpretty

💡 I recommend setting TEST_IOS_VERSION and TEST_IOS_SIMULATOR_MODEL as environment variables so that failures after Xcode updates can be resolved without changing the code.