Rejeté

« Deux motifs de rejet. L’un était de ma faute. L’autre, presque. »

L’e-mail est arrivé quelques jours après la soumission. Rien d’alarmant dans l’objet. App Store Connect affichait deux violations : 2.1(a) et 2.1(b). Un plantage, et une interface qui semblait cassée.

Apple joint un journal de plantage au rejet. Je l’ai ouvert.

Rejet nᵒ 1 : directive 2.1(a), le plantage

Le journal nommait dispatch_assert_queue_fail. Le thread : com.apple.launchservices.open-queue. La frame juste au-dessus : une fermeture dans DiagnosticsViewModel.openApp().

Exception Type:  EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000001, 0x00000001xxxxxxxx
Crashed Thread:  com.apple.launchservices.open-queue

Thread 7 Crashed:: com.apple.launchservices.open-queue
0   libdispatch.dylib       dispatch_assert_queue_fail + 120
1   AppKit                  -[NSWorkspace _callCompletionHandler:...] + 88
2   Renala                  closure #1 in DiagnosticsViewModel.openApp() + 64
    // ^^^ This is the empty { _, _ in } closure.
    //     Swift 6 marked it @MainActor.
    //     It's running on the launch services queue.
    //     dispatch_assert_queue_fail fires.

Le code appelant ressemblait à ceci :

NSWorkspace.shared.openApplication(
    at: url,
    configuration: config,
    completionHandler: { _, _ in }
)

Une fermeture vide, passée parce que l’API attend un completionHandler. Pour un développeur, l’équivalent d’un hochement de tête poli.

Voilà ce qui s’est passé. DiagnosticsViewModel est @MainActor. En mode strict concurrency, Swift 6 infère que toute fermeture définie dans un contexte @MainActor est elle-même @MainActor. Donc {'{'{'}'} _, _ in {'}'} est devenu @MainActor {'{'{'}'} _, _ in {'}'}.

Sauf que NSWorkspace.openApplication(at:configuration:completionHandler:) appelle le completionHandler sur com.apple.launchservices.open-queue, pas sur l’acteur principal. Sur macOS Sequoia, Apple a ajouté une assertion à l’exécution : si un completionHandler annoté @MainActor tourne sur une autre file d’attente, dispatch_assert_queue_fail se déclenche et le processus plante.

macOS Sequoia n’était pas installé sur ma machine de développement. Évidemment.

Le correctif tient en une ligne supprimée. Si le completionHandler ne fait rien, on le retire :

NSWorkspace.shared.openApplication(at: url, configuration: config)

Plus de fermeture, plus d’annotation de type, plus de violation d’assertion.

La tension est réelle. Swift 6 a rendu l’annotation automatique, et elle était techniquement correcte : la fermeture était bien @MainActor. Mais l’API l’exécute sur une autre file d’attente. Avant macOS Sequoia, cette incohérence passait inaperçue. Après, ça plante. Le compilateur ne peut rien y faire : la contrainte vient du runtime, pas du système de types.

La leçon : une fermeture vide n’est pas gratuite. En Swift 6, même ne rien faire a un type, et ce type a son avis sur l’endroit où il s’exécute.

Les frames clés du journal de plantage, annotées :

Exception Type:  EXC_BREAKPOINT
Exception Subtype: SIGTRAP

Thread 0 Crashed:: Dispatch main queue
  ...
  dispatch_assert_queue_fail
  ...
  NSWorkspace.openApplication(at:configuration:completionHandler:)
  ...
  // Fix: omit the completionHandler entirely if unused

Rejet nᵒ 2 : directive 2.1(b), la boîte à pourboires vide

Le second rejet était le plus instructif. La boîte à pourboires apparaissait vide pendant la validation. Pas de produits, pas d’erreur. Juste un panneau vide là où les options de pourboire auraient dû s’afficher.

La cause : Product.products(for:) renvoie un tableau vide pour les achats intégrés au statut « Waiting for Review ». Le bac à sable de validation de l’App Store ne voit pas les produits non encore approuvés. L’application et ses achats intégrés sont examinés ensemble, mais les produits restent invisibles pour StoreKit tant que la validation n’est pas passée.

Les pourboires ne peuvent pas fonctionner pendant la fenêtre de validation. Ce n’est pas un bogue. Mais un panneau vide sans explication en a tout l’air. Le réviseur voit une interface qui semble cassée. Il la signale sous 2.1(b).

Le correctif : détecter un tableau de produits vide et afficher un message plutôt que rien.

if products.isEmpty {
    Text("Tips are not available right now.")
        .foregroundStyle(.secondary)
} else {
    // tip buttons
}

Un panneau vide a l’air cassé. Un message a l’air voulu. Parfois, ce qui sépare un bogue d’un comportement normal, c’est juste un libellé.

Les deux rejets, leurs causes et leurs correctifs :

La resoumission

Les deux correctifs ensemble. Celui du plantage a supprimé une ligne. Celui de la boîte à pourboires a ajouté un affichage conditionnel. Aucun des deux n’a touché au reste du code.

Incrémenter la version, taguer, laisser le pipeline tourner. La seconde validation a été plus rapide que la première.

Lire un journal de plantage

La structure est toujours la même : le type d’exception en haut, le thread fautif identifié, les frames listées de la plus proche du plantage vers le haut. La frame qui compte est généralement la 2 ou la 3, après l’assertion système, là où commence votre code.

Ici, trois éléments racontaient toute l’histoire. La frame nommait la fermeture exacte. Le nom de la file d’attente figurait dans le libellé du thread. Le nom de l’assertion, dispatch_assert_queue_fail, décrivait précisément ce qui avait déraillé. À partir de là, la cause était limpide.

Apple n’inclut pas l’annotation de type Swift dans le journal de plantage. Pour ça, il faut lire le code source. Mais le journal pointe au bon endroit. Remonter le source à partir d’une frame précise est plus rapide qu’un débogage à l’aveugle.

Le plantage se produisait sur une version de macOS non installée localement. Le correctif tenait en une suppression de ligne. Le prix de ne pas l’avoir détecté plus tôt : 3 cycles de soumission, quelques semaines de délai supplémentaires.

C’est ce genre de coût qui pousse à tester sur toute la gamme de versions d’OS supportées, ou au minimum à lancer une VM avant de resoumettre. Le plantage était évident une fois le journal en main. Ce qui manquait, c’était de quoi le reproduire avant la soumission.

References