Tester les mises à jour optimistes iOS sur un Mac cloud

Tester les mises à jour optimistes iOS sur un Mac cloud

Tester les mises à jour optimistes iOS sur un Mac cloud

Le bouton de favori change de couleur dès qu’on appuie dessus, mais revient à un état incorrect après un échec réseau. Après une déconnexion suivie d’une nouvelle connexion, une ancienne requête modifie l’interface du nouvel utilisateur. Nul besoin de compter sur les aléas d’un réseau lent pour reproduire ces défauts. Dans une tâche de build sur un Mac cloud, il suffit d’isoler la mise à jour optimiste dans une machine à états, puis de lui transmettre des événements de réussite, d’échec et de changement de session dans un ordre précis pour les reproduire de façon fiable.

Définir d’abord un contrat : une seule écriture en cours par enregistrement

Cet article retient la stratégie la plus simple à valider : autoriser au maximum une écriture en cours par enregistrement. Tant que la requête n’est pas terminée, un nouvel appui ne déclenche aucune requête supplémentaire. L’interface conserve la valeur provisoire et indique que l’enregistrement est en cours. On renonce aux basculements rapides et successifs, mais on obtient une cible de retour arrière sans ambiguïté.

L’état comprend trois éléments : la valeur confirmée, la valeur affichée et le ticket de l’écriture en cours. Le démarrage d’une opération ne modifie que la valeur affichée. En cas de réussite, la valeur renvoyée par le serveur met à jour les deux valeurs ; en cas d’échec certain, l’affichage revient à la valeur confirmée. Chaque enregistrement de la liste possède son propre état, sans indicateur booléen global.

Une mise à jour optimiste affiche une intention à l’avance ; elle ne prouve pas que l’enregistrement a réussi. Un ticket côté client ne remplace pas non plus le contrôle de l’ordre des écritures côté serveur.

Délimiter les cas où le résultat reste inconnu

Un délai d’attente dépassé ou une connexion interrompue ne signifie pas que le serveur a refusé l’écriture. On peut alors retirer le retour visuel provisoire, mais il faut passer dans un état en attente de synchronisation et relire les données du serveur avant d’autoriser de nouvelles actions. La branche d’échec ci-dessous représente uniquement un refus métier dont on sait qu’il n’a entraîné aucune validation de l’écriture. Elle ne doit pas servir à traiter directement toutes les erreurs réseau.

Implémenter une machine à états indépendante de l’interface

Enregistrez le contenu suivant dans optimistic-check.swift. Chaque ticket utilise un identifiant distinct, qui n’est pas réutilisé lors d’un changement de session. Le modèle ne lance lui-même aucune opération réseau et ne dépend d’aucun framework d’interface. Il peut donc s’exécuter directement sur un nœud équipé des outils en ligne de commande.

import Foundation

struct FavoriteState {
    private(set) var confirmed: Bool
    private(set) var visible: Bool
    private(set) var pending: UUID?

    init(_ value: Bool) {
        confirmed = value
        visible = value
    }

    mutating func begin(_ desired: Bool) -> UUID? {
        guard pending == nil else { return nil }
        let ticket = UUID()
        pending = ticket
        visible = desired
        return ticket
    }

    mutating func finish(_ ticket: UUID, saved: Bool?) {
        guard pending == ticket else { return }
        if let saved {
            confirmed = saved
        }
        visible = confirmed
        pending = nil
    }

    mutating func replaceSession(with value: Bool) {
        pending = nil
        confirmed = value
        visible = value
    }
}

La valeur optionnelle de finish suit une convention précise : une valeur présente représente l’état final confirmé par le serveur, tandis que nil indique que l’écriture n’a, de façon certaine, pas été validée. Le serveur peut appliquer des règles métier et remplacer la valeur demandée par une autre. Le chemin de réussite ne doit donc pas se contenter de recopier la valeur souhaitée au moment de l’appui.

Dans le code de production, placez la machine à états sur l’exécuteur principal et revenez sur ce même exécuteur pour la modifier une fois l’opération réseau terminée. N’appelez pas ces méthodes simultanément depuis deux files de callbacks : un type valeur n’élimine pas automatiquement les accès concurrents aux données.

Remplacer l’attente réseau par une séquence d’événements

Ajoutez les assertions suivantes à la fin du même fichier. Elles ne nécessitent ni temporisation ni requête réelle. Même si les identifiants des tickets sont aléatoires, les vérifications reposent uniquement sur des comparaisons d’égalité : leur résultat reste déterministe.

var state = FavoriteState(false)

let first = state.begin(true)!
precondition(state.visible && !state.confirmed)
precondition(state.begin(false) == nil)
state.finish(first, saved: nil)
precondition(!state.visible && state.pending == nil)

let second = state.begin(true)!
state.finish(second, saved: true)
precondition(state.visible && state.confirmed)
state.finish(second, saved: false)
precondition(state.visible)

let oldSession = state.begin(false)!
state.replaceSession(with: false)
let current = state.begin(true)!
state.finish(oldSession, saved: false)
precondition(state.pending == current && state.visible)
state.finish(current, saved: false)
precondition(!state.visible && !state.confirmed)

Exécutez le programme comme suit. N’ajoutez pas à cette compilation d’option d’optimisation qui désactive les vérifications de préconditions.

set -eu
xcrun swiftc optimistic-check.swift -o optimistic-check
./optimistic-check

Un code de sortie non nul doit bloquer la tâche. Le premier groupe vérifie le retour arrière après un échec et les appuis répétés ; le deuxième vérifie les callbacks reçus plusieurs fois. Le troisième couvre les réponses d’une ancienne session ainsi que le cas où la valeur renvoyée par le serveur diffère de l’intention de l’utilisateur. Il n’est pas nécessaire d’afficher les identifiants de compte ni le corps complet des réponses.

Préserver l’identité des enregistrements lors du raccordement au réseau

Une requête réelle doit capturer l’identifiant de l’enregistrement, l’identité de la session courante et le ticket. Au retour, vérifiez d’abord que la réponse correspond toujours au même enregistrement, puis transmettez le résultat à la machine à états concernée. Lorsqu’une cellule de liste est réutilisée, son numéro de ligne ne suffit pas à retrouver la cible : le tri et le filtrage peuvent le modifier.

Lors d’un changement de compte, remplacez l’état de tous les enregistrements et rechargez les données faisant autorité. Dans l’exemple, replaceSession illustre la réinitialisation effectuée après réception de la valeur initiale de la nouvelle session. Cette méthode n’autorise pas à continuer d’utiliser l’ancienne valeur après la déconnexion.

L’annulation d’une requête réduit seulement le travail inutile ; elle ne remplace pas la vérification du ticket. Au moment de l’annulation, le serveur peut déjà avoir validé l’écriture et le callback peut déjà être en attente d’exécution. Si plusieurs appareils modifient simultanément le même enregistrement, le serveur doit aussi fournir des conditions de version ou des résultats signalant un conflit. La stratégie locale limitant les écritures en cours ne couvre pas la concurrence entre appareils.

Faire des vérifications d’état un contrôle bloquant avant publication

Avant de fusionner le code, ajoutez trois catégories de vérifications : le bouton redevient utilisable après un refus métier ; un résultat indéterminé bloque toute nouvelle écriture et déclenche une synchronisation ; après un changement de compte, les anciens callbacks ne modifient pas le nouvel état. En complément des vérifications de la machine à états, utilisez des tests d’interface pour confirmer que l’indication d’enregistrement en cours, l’état d’accessibilité et le blocage des appuis sont bien liés à pending.

Si vous détectez un retour arrière incorrect, conservez d’abord la séquence d’événements, une version anonymisée de l’identifiant de l’enregistrement et la catégorie de résultat. Vérifiez ensuite si un ancien numéro de ligne a été réutilisé par erreur ou si des modèles de sessions différentes ont été mélangés. N’allongez pas les délais d’attente pour masquer le problème. Le critère final de validation est que chaque changement d’état corresponde à un événement, et non que le bouton finisse par afficher le bon état par hasard.

Questions fréquentes

Un délai dépassé signifie-t-il que le serveur n’a rien enregistré ?

Non. Le résultat est inconnu. Après avoir annulé l’affichage provisoire, relisez l’état du serveur avant d’autoriser une nouvelle écriture.

Un ticket local évite-t-il les conflits d’écriture sur le serveur ?

Non. Il empêche seulement une ancienne réponse de modifier l’interface actuelle. Le serveur doit sérialiser les écritures ou vérifier une version.

Ajoutez un Mac physique à votre pipeline

Choisissez un Apple Silicon M4 ou M4 Pro : chaque machine est réservée à un seul client, sans virtualisation. Sélectionnez le modèle adapté à vos charges de compilation, puis la formule de facturation qui convient à votre équipe.

Louer un nœud physique

Confiez votre prochaine compilation à un nœud dédié

Vérifiez les caractéristiques matérielles et la formule de facturation, puis équipez votre équipe d’un Mac physique dédié.