En attente
« Le travail est fait. La suite ne dépend plus de moi. »
App Store Connect affiche « Waiting for Review ». Un badge jaune dans un onglet de navigateur : des mois de travail, réduits à un rang dans une file.
Plus rien à corriger, plus rien à optimiser. L’application est aussi aboutie que je peux la rendre, et maintenant quelqu’un chez Apple va décider si elle respecte les guidelines. J’ai mis la cafetière en route.
L’écart
Le binaire était prêt des semaines avant sa publication. Ce qui a comblé l’écart, ce n’était pas du code.
Première soumission, premier rejet. Puis un deuxième, puis quatre autres. Six avis sur quatre versions, étalés sur trois semaines. Chaque correction prenait un jour, parfois moins. Le cycle de validation prenait le reste. L’application était prête dès la première soumission. Le processus, non.
Le mauvais diagnostic
Un rejet revenait sans cesse : la boîte à pourboires apparaissait vide. Quatre avis, quatre messages identiques. « In-app purchase products could not be found in the submitted binary. »
Mon explication initiale était plausible, cohérente, et fausse. Je supposais que les produits étaient en « Waiting for Review » et que le bac à sable d’Apple ne pouvait tout simplement pas les voir. Les réponses d’Apple ne démentaient pas cette hypothèse. Les mêmes suggestions revenaient à chaque fois : vérifier que les produits sont actifs, s’assurer de l’implémentation StoreKit, soumettre à nouveau.
Pendant ce temps, la boîte à pourboires fonctionnait parfaitement sur ma machine. À chaque fois. Le scheme Xcode incluait un fichier de configuration StoreKit local qui interceptait tous les appels Product.products(for:) et servait les produits depuis un JSON local, sans jamais passer par les serveurs d’Apple. L’équivalent, pour les tests, d’un détecteur de fumée à piles mortes : tout semble normal jusqu’à ce que la maison brûle et que quelqu’un d’autre s’en aperçoive en premier.
La vraie cause : un contrat Applications payantes, sur une page Business d’App Store Connect que j’ignorais. Le contrat exige des coordonnées bancaires, des formulaires fiscaux et une déclaration de conformité au Digital Services Act européen. Rien de tout cela n’était rempli avant la première soumission. Ni avertissement au moment de soumettre, ni validation. Les produits IAP affichaient « Ready to Submit » et semblaient correctement configurés. Apple a nommé ce contrat uniquement après une question directe lors du cinquième échange, au bout de douze jours.
On ne sait pas ce qu’on ne sait pas, et le système ne tient personne par la main.
Leçons
L’outillage est plus dur que le langage. La syntaxe et les bases de Swift s’apprennent en quelques semaines quand on vient d’un langage typé. Xcode, les certificats de signature, les entitlements, le bac à sable, les profils de provisionnement, la notarisation, le contrat Applications payantes : tout ça prend plus de temps. Le langage, c’est la partie facile. La plateforme, c’est le vrai parcours du combattant.
Le compilateur joue pour nous. La vérification stricte de concurrence en Swift produit des erreurs qui semblent hostiles tant qu’on n’a pas compris ce qu’elles disent. Tous les avertissements ne correspondent pas à une vraie course de données, mais dans ce projet, la majorité révélaient de vrais bugs de concurrence. Une erreur d’isolation d’acteur, ce n’est pas de la bureaucratie : c’est le compilateur qui signale un chemin de code qu’il ne peut pas prouver sûr. À nous d’en faire la preuve. Les erreurs du compilateur sont de l’information, pas des obstacles. Les messages de rejet d’Apple, en revanche, ne jouent pas pour nous. Quatre réponses identiques, douze jours, et la cause racine n’a été nommée qu’après une question directe.
La documentation Apple est bonne quand elle existe. Quand elle n’existe pas, on lit les headers et les transcriptions de sessions WWDC d’il y a trois ans. La référence API reste à jour parce qu’elle est générée à partir des headers. Les guides conceptuels et les documents de migration vieillissent plus vite.
La performance exige de comprendre la plateforme, pas seulement le langage. Le développement web cache le système sous-jacent derrière HTTP, JSON et les conventions du framework ; le développement natif macOS, non. getattrlistbulk, un appel système Darwin qui récupère les attributs de fichiers en lot par répertoire, face à FileManager.enumerator, qui interroge le noyau fichier par fichier. Le scanner getattrlistbulk était environ 1,7 fois plus rapide. Cet écart est invisible sans mesure.
La plateforme va au-delà de la surface de l’API. Les files de dispatch et les assertions qui sanctionnent tout blocage du thread principal. La différence entre mémoire résidente et mémoire virtuelle. Ce ne sont pas des sujets exotiques : c’est la plateforme.
Les outils donnent de l’information, pas du jugement. J’ai utilisé GitHub Copilot tout au long du projet. Pour les décisions d’architecture, beaucoup moins : il ne connaît pas les contraintes du projet. Les plantages, l’optimisation mémoire, la découverte de getattrlistbulk, le contrat Applications payantes : tout ça a exigé de lire la documentation, d’utiliser Instruments, de comprendre le système. Copilot peut aider à écrire le code une fois qu’on sait quoi écrire. Savoir quoi écrire, c’est la partie difficile, et ça l’a toujours été.
Ce que je ferais autrement
Commencer par getattrlistbulk. J’ai construit le scanner FileManager d’abord parce que c’était le choix évident, et cette première version m’en a appris assez pour rendre la réécriture lisible. Mais l’écart de performance était suffisant pour que je regrette de ne pas avoir exploré l’API bas niveau plus tôt.
Ajouter PerfLogWriter plus tôt. L’infrastructure de journalisation qui a permis de repérer les blocages du thread principal n’existait pas encore quand les blocages sont apparus. Ce sont les blocages qui m’ont indiqué où mesurer, mais avoir l’outillage prêt aurait raccourci le diagnostic.
Retirer les achats intégrés et publier. La boîte à pourboires a bloqué toutes les autres corrections pendant douze jours. La correction du crash, la description d’usage, la soumission propre : tout était en otage d’une fonctionnalité optionnelle dont la cause racine était administrative. Publier l’application fonctionnelle, régler le contrat séparément.
Deux choses que je ne changerais pas : la localisation dès le départ, et la rédaction des Architecture Decision Records (ADR). Extraire les chaînes pour la localisation après coup, c’est pénible. Les garder externalisées au fil du développement, rien qu’une discipline. Les traductions sont venues plus tard, mais l’infrastructure était là dès le départ. L’application est sortie en 11 langues : ça ne serait pas arrivé en s’y prenant après coup. Les ADR, ce sont 41 documents sur des choix qui ne coulaient pas de source. Pendant le débogage du rejet App Store, j’ai pu retrouver exactement pourquoi NSWorkspace.openApplication était appelé de cette façon, et quelles alternatives avaient été envisagées : plus rapide que de fouiller le git blame pour deviner l’intention.
Le chiffre qui me surprend encore
5,2 millions de nœuds sur un scan complet du disque : c’est ce que contient réellement un Mac de développeur bien rempli. Fichiers, répertoires, liens symboliques, énumérés, dimensionnés, disposés dans un treemap en environ une minute (compilation Debug, M3 Pro). L’ordre de grandeur est réel.
Quand j’ai démarré ce projet, je ne savais pas si c’était faisable : la réponse a exigé de choisir le bon appel système, de comprendre pourquoi FileManager était lent, et de tout mesurer.
Le projet en chiffres :
| Métrique | Valeur |
|---|---|
| Commits | ~353 |
| ADR | 41 |
| Langues | 11 |
| Cycles de soumission | 6 |
Gain de scan (getattrlistbulk vs FileManager) | ~1,7x |
| Mémoire (avant) | 1,2 Go |
| Mémoire (après) | 469 Mo |
| Détection de doublons | ~1,3 à ~397 groupes/s |
L’application est dans la file d’attente. Le disque n’est plus plein : parfois, le problème le plus simple produit l’ingénierie la plus intéressante.
Références
- Apple : Directives de la page produit App Store
- Apple : Signer et mettre à jour les contrats
- GitHub : GitHub Copilot
