Automatiser la gestion de ses simulateurs iOS

Jun 6, 2023 • Thibaut Coutard

Chez Lucca nous éditons deux applications iOS, Timmi et Cleemy. Chaque application impose une version d'OS minimum différente (iOS 15 pour Timmi et iOS 16 pour Cleemy).

Dans le cadre des tests et de la QA, cette spécificité impose d'installer plusieurs simulateurs. Pour les tests end-to-end (que nous utilisons également pour créer nos captures d'écrans) nous avons besoin de 4 appareils par version d'OS, donc 8 simulateurs. Pour les tests unitaires nous n'utilisons que deux simulateurs (un par version d'OS). Et les mises à jour de Xcode qui suppriment parfois les simulateurs, mettent à mal la stabilité de la CI.

Enfin, nous recherchons une "portabilité" et un "versionnement" de la CI pour pouvoir la lancer sur n'importe quelle machine, que ce soit une nouvelle machine de CI ou en local sur nos postes de développeurs.

Le point clé est donc de maîtriser les simulateurs : connaître ceux en place et être capable d'installer les manquants.

C'est l'objet de cet article.

xcrun simctl à la rescousse

Dans le cadre de l'automatisation sur iOS que ce soit pour de la CI ou de simples scripts, on utilise souvent une commande très utile xcrun. Cet outil permet de retrouver et utiliser les outils de developpement d'Xcode. On pense par exemple à iTMSTransporter pour envoyer nos apps et métadatas sur le store, xcode-select pour gérer plusieurs versions d'xcode ou celui qui nous intéresse ici : simctl pour gérer nos simulateurs.

On peut lister les simulateurs et OS via

$> xcrun simctl list
== Device Types ==
iPhone 6s (com.apple.CoreSimulator.SimDeviceType.iPhone-6s)
...
== Runtimes ==
iOS 14.5 (14.5 - 18E182) - com.apple.CoreSimulator.SimRuntime.iOS-14-5
...
== Devices ==
-- iOS 14.5 --
    iPhone 8 (B19ED49F-2847-4C24-997C-2DFF5F1BE137) (Shutdown)
...

On peut les filtrer via

$> xcrun simctl list devices "iOS 16.4" available
== Devices ==
-- iOS 15.0 --
-- iOS 16.0 --
-- iOS 16.4 --
    iPhone 8 Plus (5E79EB8C-3AFA-4932-B9DE-C8CBE72E3EC3) (Shutdown) 
    iPhone 11 Pro Max (82601E2C-6E5F-4168-B9B1-5645B00143BD) (Shutdown) 
    iPhone 14 Pro Max (3C88C99B-1BE6-47BE-B4F0-FFD66A92EEA9) (Shutdown) 
    iPad Pro (12.9-inch) (2nd generation) (358DEB04-3438-4C79-9D71-72DAA199B96B) (Shutdown) 
    iPad Pro (12.9-inch) (3rd generation) (0A7724BA-7065-480A-992E-87507F3D270F) (Shutdown) 

On peut créer un simulateur via

$> xcrun simctl create "iPad Pro (12.9-inch) (2nd generation)" "iPad Pro (12.9-inch) (2nd generation)" "iOS16.4"
D996F5C4-AF2D-4159-8EB2-42E6A71CD1DD

xcrun simctl create prend un paramètre, le nom du simulateur, l'appareil simulé et la version de l'OS que l'on veut.

Et voilà ! On a maintenant tout ce qu'il nous faut pour gérer nos simulateurs.

Place à la magie

Pour tout remettre ensemble, je vous propose de le faire en ruby pour l'intégrer facilement avec Fastlane (un outil d'automatisation pour iOS très utilisé pour les CI). Commençons par vérifier que nos simulateurs existent.

simulator_exists

desc 'Check if simulator exists parameters: os_version (iOS 16.0) and device (iPhone 8)'
private_lane :simulator_exists do |options|
  result = sh("xcrun simctl list devices \"#{options[:os_version]}\" available | grep \"#{options[:device]} (\" | wc -l") # 1
  !result.include? '0' # 2
end

1- Décortiquons d'abord la première ligne

  • options[:os_version] correspond au paramètre de la version qu'on veut vérifier sous le format : iOS 16.0.
  • options[:device] correspond au paramètre de l'appareil qu'on veut vérifier sous le format : iPhone 8 Plus.
  • xcrun simctl list devices \"#{options[:os_version]}\" available ici on récupère donc tous les appareils avec la version de l'OS et si ils sont disponibles.
  • grep \"#{options[:device]} (\" va filtrer sur l'appareil en question. On a ajouté dans le filtre ( pour ne pas avoir de faux positifs par exemple en cherchant un iPhone 8 tomber sur iPhone 8 et iPhone 8 Plus.
  • wc -l va compter les occurrences de notre filtre récupéré via le grep

2- Ici on vérifie simplement que le résultat de notre recherche est différent de 0. En ruby, il n'y a pas besoin de mettre de return, par défaut le résultat de la dernière ligne sera envoyé en retour.

Ce premier script permet donc de vérifier si notre simulateur existe. Il nous manque maintenant à le recréer.

create_simulator

desc 'Create simulator parameters: os_version (iOS 16.0) and device (iPhone 8)'
private_lane :create_simulator do |options|
  os_version = options[:os_version]
  os_version = os_version.gsub(' ', '') #1
  sh("xcrun simctl create \"#{options[:device]}\" \"#{options[:device]}\" \"#{os_version}\"") # 2
end

1- On nettoie la version pour retirer les espaces pour avoir un format du type iOS16.4

2- On peut maintenant appeler notre xcrun simctl create avec nos paramètres.

Et maintenant comment mixer nos deux scripts ?

desc 'create simulator if does not exist parameters: os_version (iOS 16.0) and device (iPhone 8)'
lane :create_simulator_if_dont_exist do |options|
  unless simulator_exists(os_version: options[:os_version], device: options[:device])
    create_simulator(os_version: options[:os_version], device: options[:device])
  end
end

Ici on vérifie l'existence du simulateur et on le crée si il n'existe pas.

Cas réel

Voici un exemple d'utilisation de notre script pour s'assurer que tous nos simulateurs sont installés pour lancer nos captures d'écran automatique. On lance ce script avant de créer nos captures d'écrans pour l'Apple Store ou pour préparer un poste de travail.

desc 'Recreate all simulators if needed'
lane :recreate_all_simulators do |options|
  version = ENV["OS_TEST_E2E"] || sh('xcrun --sdk iphoneos --show-sdk-platform-version').strip
  default_devices = [
    'iPhone 11 Pro Max',
    'iPhone 8 Plus',
    'iPad Pro (12.9-inch) (2nd generation)',
    'iPad Pro (12.9-inch) (3rd generation)'
  ]
  if version.to_f >= 16.0
    default_devices.append('iPhone 14 Pro Max')
  end

  devices = options[:devices] || default_devices # 1
  devices.each do |device| 
    create_simulator_if_dont_exist(os_version: "iOS #{version}", device: device) # 2
  end
end

1- On définit nos appareils. J'ai ajouté une valeur par défaut ajoutant 4 appareils (iPhone 11 Pro Max, iPhone 8 Plus, iPad Pro (12.9-inch) (2nd generation), iPad Pro (12.9-inch) (3rd generation)) plus un 5eme si on est en iOS 16.0 (iPhone 14 Pro Max). Ça me permet de lancer cette lane sans paramètre et donc tout installer par défaut.

J'ai fait la même chose avec la version de l'OS en vérifiant si j'en ai un dans l'environnement et en laissant une option par défaut qui récupère le résultat de xcrun --sdk iphoneos --show-sdk-platform-version.

2- On va ensuite appeler notre lane create_simulator_if_dont_exist avec chaque appareil de notre liste.

Autre cas réel

Chez Lucca nous lançons des tests end-to-end pour chaque Pull Requests. Ça nous permet de nous assurer que rien n'a cassé mais également d'envoyer des captures d'écrans sur la pull request afin de voir les changements d'UI (ça ce sera pour un prochain article 🤫). Voici le début du script :

desc 'Run E2E tests'
lane :end_to_end_tests do
  version = ENV['OS_TEST_E2E'] || '16.4' # 1
  device = ENV['DEVICE_TEST_E2E'] || 'iPhone 8 Plus' # 2
  create_simulator_if_dont_exist(os_version: "iOS #{version}", device: device) # 3

  capture_screenshots( # 4
    devices: [
      device
    ],
    ios_version: version,
    override_status_bar: true,
    languages: 'fr-FR',
    scheme: ENV['SCHEME_TEST_E2E'],
    concurrent_simulators: false,
    stop_after_first_error: false,
    clear_previous_screenshots: true,
    skip_open_summary: true,
    disable_package_automatic_updates: true,
    xcargs: '-skipPackagePluginValidation'
    )
  (...)
end

1- Ici on récupère la version depuis l'environnement en ajoutant une version par défaut.

2- Ici on récupère l'appareil depuis l'environnement en ajoutant un appareil par défaut.

3- On recrée notre simulateur.

4- On lance la capture d'écran de notre application.

Conclusion

Avec le petit script que l'on vient de voir, il suffit à chaque application de dire sur quel appareil et quel OS il doit tourner, et le tour est joué ! J'espère que ce post vous a plu. N'hésitez pas à venir en discuter avec moi sur les réseaux !

About the author

Thibaut Coutard

iOS Software Engineer