Valider les mises à jour différentielles de listes iOS

Valider les mises à jour différentielles de listes iOS

Valider les mises à jour différentielles de listes iOS

Lorsqu’une liste subit à la fois des suppressions, des insertions et un réordonnancement, les erreurs les plus difficiles à reproduire ne viennent souvent pas d’un calcul incorrect des données, mais d’une confusion entre anciennes et nouvelles positions. Sur un Mac cloud OSXFlow, vous pouvez commencer par exécuter un script de validation en Swift pur, sans lancer de simulateur, pour vérifier que les différences permettent de reconstruire les deux états. Il ne reste ensuite qu’à relier les changements structurels validés à la couche de mise à jour de l’interface.

Définir les deux états décrits par la différence

Appelons « ancien instantané » le tableau d’identifiants stables avant la mise à jour, et « nouvel instantané » celui obtenu après. L’appel s’effectue dans le sens new.difference(from: old), et non l’inverse. Les indices de suppression se rapportent à l’ancien instantané, ceux d’insertion au nouveau : il ne faut pas les interpréter dans un même repère qui évoluerait au fil des opérations.

Prenons l’ancienne liste [a,b,c] et la nouvelle [c,a,d]. Insérer d’abord aux nouvelles positions, puis supprimer aux anciennes peut conduire à supprimer un élément qui vient d’être inséré. Pour laisser la bibliothèque standard reconstruire le résultat, utilisez old.applying(diff) plutôt que de réajuster vous-même chaque indice.

Fixez d’abord trois règles : les identifiants doivent être uniques dans chaque instantané ; une modification de contenu ne doit pas changer l’identifiant ; l’instantané source ne doit pas être modifié sur place pendant le calcul de la différence en arrière-plan. Des identifiants en double ne provoquent pas nécessairement une erreur de la bibliothèque standard, mais rendent ambiguë l’identité des lignes de l’interface.

Une différence n’est pas un correctif applicable à n’importe quelle liste. Elle correspond à une paire précise d’instantanés ; le fait de pouvoir l’appliquer ne garantit pas que la version des données source soit la bonne.

Vérifier l’aller-retour avec un script exécutable

Dans le terminal distant, vérifiez que xcrun --find swift trouve le compilateur. Enregistrez le code ci-dessous dans /tmp/list-diff-check.swift, puis exécutez xcrun swift /tmp/list-diff-check.swift. Cet exemple ne dépend que de la bibliothèque standard : aucun projet, téléchargement de dépendances ni réglage de signature n’est nécessaire.

func hasUniqueIDs(_ values: [String]) -> Bool {
    Set(values).count == values.count
}

func verify(_ old: [String], _ new: [String]) {
    precondition(hasUniqueIDs(old))
    precondition(hasUniqueIDs(new))

    let raw = new.difference(from: old)
    let moved = raw.inferringMoves()

    precondition(old.applying(raw) == new)
    precondition(old.applying(moved) == new)
    precondition(new.applying(moved.inverse()) == old)
}

verify([], [])
verify([], ["a"])
verify(["a"], [])
verify(["a", "b", "c"], ["c", "a", "d"])
verify(["a", "b", "c"], ["c", "b", "a"])
precondition(!hasUniqueIDs(["a", "a"]))

Si tout se passe bien, le script ne produit aucune sortie et le processus se termine avec un code de retour nul. L’échec d’une seule précondition interrompt l’exécution et fait échouer le pipeline. N’utilisez pas le mode de compilation -Ounchecked, qui supprime ces vérifications.

Le script vérifie à la fois la différence brute, la différence après inférence des déplacements et la transformation inverse. Ces trois contrôles répondent respectivement aux questions suivantes : « Peut-on atteindre l’état cible ? », « Les associations de déplacement préservent-elles la sémantique ? » et « Peut-on revenir à l’état source ? ». Vérifier uniquement le nombre d’éléments ne permet pas de détecter les erreurs de positionnement : il faut comparer les tableaux complets.

Explorer exhaustivement les petits ensembles plutôt que quelques cas évidents

Les cas fixes sont utiles pour expliquer un défaut, mais laissent facilement de côté des combinaisons telles que « supprimer le premier élément tout en déplaçant le dernier au milieu ». Avec quatre identifiants distincts, il existe 65 sous-ensembles ordonnés sans répétition, soit 4225 transitions entre paires. Cette taille permet une exploration exhaustive, sans graine aléatoire.

Ajoutez le code suivant à la fin du même fichier, puis relancez le script :

func snapshots(
    prefix: [String],
    remaining: [String]
) -> [[String]] {
    var result = [prefix]
    for index in remaining.indices {
        var rest = remaining
        let next = rest.remove(at: index)
        result += snapshots(
            prefix: prefix + [next],
            remaining: rest
        )
    }
    return result
}

let cases = snapshots(
    prefix: [],
    remaining: ["a", "b", "c", "d"]
)
precondition(cases.count == 65)

for old in cases {
    for new in cases {
        verify(old, new)
    }
}

Ce test couvre la liste vide, les insertions seules, les suppressions seules, l’inversion complète et les modifications mixtes, sans pour autant démontrer la correction pour des longueurs arbitraires. Lors de l’intégration, réduisez chaque incident de production à une paire minimale d’entrées, puis ajoutez-la aux cas fixes. Pour les grandes listes, mesurez séparément le temps d’exécution et la consommation mémoire : les résultats obtenus ici ne constituent pas une garantie de performances.

Une association de déplacement n’est pas une instruction de déplacement pour l’interface

inferringMoves() associe les suppressions et les insertions qui peuvent être reconnues comme des déplacements, sans les transformer en un nouveau cas d’énumération. Lorsque vous passez la différence à applying(_:), conservez-la dans son intégralité : la présence d’une association ne justifie pas d’ignorer l’une des deux opérations.

Si l’interface utilise sa propre API de mises à jour par lots, implémentez un adaptateur distinct et vérifiez ses exigences concernant les positions source, les positions cibles et le moment où la source de données doit être mise à jour. La validation de la différence de la bibliothèque standard prouve seulement que la transformation du tableau est correcte, pas qu’un ordre quelconque d’appels à l’interface le soit aussi.

Traiter les actualisations de contenu séparément

Supposons que seul le titre de la ligne d’identifiant a change. Le tableau d’identifiants reste identique, donc la différence est vide. Vous devez malgré tout comparer les champs visibles des anciens et des nouveaux modèles pour recueillir les identifiants à actualiser. Ne générez pas un nouvel identifiant uniquement pour déclencher cette actualisation : vous transformeriez une modification de contenu en suppression suivie d’une insertion, avec perte de l’état de sélection ou de dépliage.

Bloquer les différences obsolètes par un contrôle de version

Au démarrage du calcul en arrière-plan, capturez à la fois l’ancien instantané et un numéro de version strictement croissant. Lorsque le résultat revient dans le domaine d’isolation de l’interface, vérifiez d’abord que la version courante correspond toujours à la version capturée, puis mettez à jour le modèle et l’interface. Aucune attente asynchrone ne doit s’intercaler entre cette vérification et la validation de la mise à jour.

Si la version a changé, abandonnez le résultat et recalculez la différence à partir de l’instantané courant. Ne considérez pas le fait que la bibliothèque standard renvoie un résultat non nul comme une validation de version : des valeurs répétées ou des positions compatibles par hasard peuvent permettre à une différence obsolète de produire tout de même un tableau.

Avant la mise en production, vérifiez une dernière fois quatre points : les identifiants en double sont explicitement rejetés ; les changements structurels sont séparés des actualisations de contenu ; aucune version obsolète n’est validée ; l’adaptateur d’interface est testé sur au moins un cas mêlant suppression, déplacement et insertion. Conserver les instantanés d’entrée, le numéro de version et l’assertion ayant échoué suffit à localiser le problème, sans journaliser de contenu sensible des lignes.

Questions fréquentes

Peut-on supprimer les opérations associées après inferringMoves() ?

Non. Un déplacement reste représenté par une suppression et une insertion associées. Conservez la différence complète pour applying(_:) ; toute conversion en opération d’interface demande un adaptateur testé.

Une différence calculée sur les identifiants détecte-t-elle un changement de titre ?

Non. Les identifiants décrivent la structure de la liste. Comparez séparément le contenu pour déclencher son rafraîchissement, puis utilisez le modèle actualisé.

Ajoutez un Mac dédié à votre workflow

Utilisez macOS natif pour compiler, tester et expérimenter. Comparez les configurations et les durées de facturation, puis choisissez le nœud physique qui vous convient.

Louer un Mac dans le cloud Voir les configurations

Confiez votre prochaine compilation à un Mac dans le cloud

Un ordinateur physique dédié, pas une machine virtuelle. Choisissez la configuration et la durée de location adaptées à votre projet.