Lors d’un traitement de fichiers par lot, la barre de progression atteint 100 %, puis une erreur apparaît ; l’utilisateur clique sur Annuler, mais le traitement en arrière-plan publie tout de même le résultat. Ce n’est généralement pas un problème d’affichage : c’est le signe que la quantité de travail, le signal d’annulation et l’état final de l’opération ont été confondus. Sur un Mac cloud GNUMac, commencez par fixer ces contrats à l’aide de tests unitaires qui ne dépendent ni du réseau ni d’un simulateur, puis vérifiez l’interface iOS.
Définir ce que mesure la progression
Le scénario de test de cet article consiste à convertir quatre entrées, puis à vérifier deux sorties. La conversion représente 60 % du travail total, et la vérification 40 %. Ces poids sont définis pour le test : ils ne proviennent pas de mesures de performances et n’indiquent pas le temps restant.
Le total de la progression parente est fixé à cent, tandis que les progressions enfants comptent chacune des fichiers. N’additionnez pas directement un nombre de fichiers convertis et un nombre d’octets envoyés : des unités différentes doivent d’abord être ramenées aux poids définis au niveau parent.
L’état métier est géré séparément : en attente, en cours, annulation en cours, annulé, échec ou réussite. L’objet de progression décrit uniquement des quantités ; il ne sert pas à classer les erreurs.
Le pourcentage indique « quelle part du travail défini est terminée » ; l’état métier indique « si le résultat a effectivement été livré de manière fiable ». L’un ne remplace pas l’autre.
Avant l’implémentation, posez trois contraintes : chaque progression enfant n’est rattachée qu’une seule fois ; la quantité terminée ne dépasse jamais le total ; toute validation définitive du résultat est précédée d’un contrôle d’annulation. Les objets parent et enfants appartiennent à une seule tâche, et non à une variable globale partagée par toutes les tâches.
Valider les poids avec des tests déterministes
Créez un fichier de test dans la cible de tests unitaires du projet existant, puis ajoutez le code suivant. Le test utilise uniquement Foundation et XCTest : il n’attend aucune conversion réelle de fichiers et ne repose pas sur des pauses pour deviner l’ordre d’exécution.
import Foundation
import XCTest
final class WeightedProgressTests: XCTestCase {
func testWeightedStages() {
let root = Progress(totalUnitCount: 100)
let convert = Progress(totalUnitCount: 4)
let verify = Progress(totalUnitCount: 2)
root.addChild(convert, withPendingUnitCount: 60)
root.addChild(verify, withPendingUnitCount: 40)
XCTAssertEqual(root.fractionCompleted, 0, accuracy: 0.000001)
convert.completedUnitCount = 2
XCTAssertEqual(root.fractionCompleted, 0.3, accuracy: 0.000001)
verify.completedUnitCount = 1
XCTAssertEqual(root.fractionCompleted, 0.5, accuracy: 0.000001)
convert.completedUnitCount = 4
verify.completedUnitCount = 2
XCTAssertEqual(root.fractionCompleted, 1, accuracy: 0.000001)
}
}
Les deux étapes progressent ici volontairement de façon entrelacée, afin de vérifier que le parent ne reflète pas uniquement l’étape en cours. Comparez les résultats en virgule flottante avec une tolérance, plutôt que de faire porter les assertions sur des pourcentages formatés en chaînes de caractères.
Exécutez cette classe depuis le navigateur de tests du Mac cloud et conservez l’identifiant du commit, les résultats des tests et les informations sur la chaîne d’outils utilisée. Si les tests disposent déjà d’un point d’entrée en ligne de commande, ajoutez-y uniquement cette cible de tests, sans créer une nouvelle matrice de simulateurs pour tester la progression.
L’annulation exige la coopération du code exécutant
Progress.cancel() émet un signal ; cette méthode ne force pas l’arrêt d’une boucle en cours. Vérifiez l’état avant de traiter chaque unité et n’incrémentez le compteur qu’une fois cette unité réellement terminée. Effectuez un nouveau contrôle juste avant le remplacement définitif du fichier de sortie.
Le test suivant vérifie qu’« aucune nouvelle unité de travail ne démarre après l’annulation ». Il ne prétend pas couvrir les conditions de concurrence et peut être ajouté directement à la classe de test précédente.
func testCancellationStopsNextUnit() {
let progress = Progress(totalUnitCount: 4)
progress.isCancellable = true
var processed = 0
func runNextUnit() {
guard !progress.isCancelled else { return }
processed += 1
progress.completedUnitCount += 1
}
runNextUnit()
progress.cancel()
runNextUnit()
XCTAssertTrue(progress.isCancelled)
XCTAssertEqual(processed, 1)
XCTAssertEqual(progress.completedUnitCount, 1)
}
Le code d’exécution asynchrone nécessite également deux points de suspension contrôlables : avant le démarrage d’une opération longue et avant la validation définitive du résultat. Le test suspend la tâche, demande l’annulation, puis autorise la reprise ; il vérifie ensuite qu’aucun nouveau travail ne démarre et qu’aucun résultat final n’est publié. Coordonnez ces étapes avec des points de synchronisation pilotés par le test, et non avec une attente d’un nombre fixe de secondes.
Si l’opération sous-jacente propose une interface d’annulation, transmettez-lui également le signal. Si elle ne peut pas être interrompue, attendez son retour, puis ignorez son résultat et supprimez les fichiers temporaires. L’interface doit rester à l’état « Annulation en cours », sans annoncer prématurément que le traitement est arrêté.
Ne pas masquer les totaux inconnus ni les nouvelles tentatives
Tant que le recensement des fichiers n’est pas terminé, affichez « Comptage en cours » plutôt que de tenter de calculer un pourcentage avec un total nul. Une fois le recensement terminé, figez la liste des tâches, établissez toutes les relations parent-enfant, puis lancez le traitement.
Augmenter continuellement le dénominateur pendant l’exécution fait reculer la progression. Si la découverte dynamique de tâches est réellement nécessaire, affichez l’état de l’étape ou le nombre d’éléments traités, et précisez que le pourcentage courant ne concerne que l’étape dont le périmètre est déjà défini.
Lors des nouvelles tentatives, ne remettez pas non plus sans cesse la même progression enfant à zéro. La couche métier peut conserver le numéro de tentative ; la couche de progression, elle, cumule les unités logiques terminées avec succès. Une tentative qui échoue ne signifie pas qu’un fichier supplémentaire a été traité.
Définissez aussi une règle spécifique pour l’absence d’entrées : si une entrée vide est valide, renvoyez directement un résultat de réussite indiquant « Aucun traitement nécessaire » ; si l’appelant doit fournir des entrées, signalez l’erreur avant le démarrage, sans créer une tâche qui restera indéfiniment à son point de départ.
Relier l’interface aux vérifications continues
Après avoir observé une modification de la progression, effectuez la mise à jour de l’état de l’interface sur l’exécuteur principal. Attribuez un identifiant de tâche distinct à chaque exécution et vérifiez cet identifiant avant toute mise à jour, afin qu’une notification tardive de l’exécution précédente n’écrase pas la nouvelle progression.
Vous pouvez arrondir le pourcentage affiché, mais ne vous servez pas de cette valeur arrondie pour déterminer la réussite. Si une vérification et une validation atomique restent nécessaires après le traitement du dernier fichier, réservez une part du travail à ces étapes. En cas d’échec, conservez l’état d’erreur au lieu de forcer le compteur à atteindre son total.
Avant la fusion, vérifiez chaque point :
- Les assertions sur les poids intermédiaires et la progression entrelacée passent, sans dépendre de la vitesse d’exécution.
- L’annulation avant le démarrage, pendant le traitement et juste avant la validation définitive dispose, dans chaque cas, d’un test distinct.
- Les chemins d’erreur ne sont pas assimilés à une réussite, et les sorties temporaires peuvent être supprimées.
- Après le démarrage d’une nouvelle tâche, les notifications de l’ancienne ne peuvent plus modifier l’interface courante.
- L’absence d’entrées, les totaux inconnus et les nouvelles tentatives ont chacun des règles d’affichage explicites.
Le livrable final n’est pas une barre de progression qui paraît fluide, mais un contrat d’état vérifiable. Rendez d’abord le décompte fiable, puis l’annulation maîtrisable, et faites enfin en sorte que la réussite signifie uniquement que le résultat est réellement utilisable.
Questions fréquentes
Une progression à 100 % signifie-t-elle que l’opération a réussi ?
Non. Le compteur mesure le travail accompli. La réussite exige aussi la validation et la livraison du résultat, suivies dans un état métier distinct.
cancel() arrête-t-il automatiquement le travail en cours ?
Non. Le code exécutant le travail doit vérifier l’annulation, la transmettre aux opérations concernées et la contrôler avant de publier le résultat.
Configurez un Mac mini dédié pour votre workflow
Choisissez le modèle, la région et la durée selon votre projet. Vérifiez la mémoire, le stockage et la connectivité avant de commander. La disponibilité réelle est indiquée en temps réel dans la console.