Un utilisateur change rapidement de filtre dans une liste iOS : la deuxième page de l’ancien filtre est encore en cours de chargement, tandis que la première page du nouveau filtre s’affiche déjà. Quelques secondes plus tard, l’ancienne réponse arrive et des éléments sans rapport apparaissent dans la liste. Ce défaut est difficile à reproduire sur un réseau stable. Faire défiler l’écran à la main, même à plusieurs reprises sur un Mac dans le cloud, ne garantit pas non plus que les deux requêtes répondront dans l’ordre voulu. Une méthode plus fiable consiste à isoler les règles de mise à jour de la pagination dans un modèle d’état, à les vérifier avec des événements ordonnés de façon déterministe, puis à raccorder le modèle à l’interface.
Définir d’abord les changements autorisés dans la liste
L’exemple ci-dessous utilise des numéros de page commençant à 1 et des identifiants métier entiers. Une véritable API peut renvoyer d’autres champs, mais le modèle doit au minimum distinguer la requête courante, la page attendue, les identifiants déjà affichés et l’existence éventuelle d’une page suivante. Chaque changement de filtre ouvre une nouvelle requête logique : même si l’ancien et le nouveau filtre renvoient finalement les mêmes éléments, une ancienne réponse ne doit pas modifier la nouvelle liste.
Attribuez à chaque nouvelle requête logique un generation croissant et associez-le au numéro de page dans un jeton de requête. Une réponse ne peut modifier la liste que si son jeton correspond exactement à celui de la requête actuellement en attente. Si un nouvel événement de fin de liste survient pendant un chargement, ignorez-le au lieu d’envoyer deux requêtes pour la même page.
Annuler une requête économise des ressources réseau et du temps de calcul ; cela ne prouve pas que son ancien callback ne s’exécutera pas. Vérifiez toujours le jeton avant de modifier la liste.
Reproduire trois scénarios d’ordonnancement avec un modèle exécutable
Enregistrez le code suivant dans Pager.swift, puis exécutez xcrun swift Pager.swift sur un Mac où les outils en ligne de commande Xcode sont installés. Le code n’utilise ni réseau ni minuterie : chaque exécution reproduit donc les mêmes scénarios problématiques.
struct Ticket: Equatable {
let generation: Int
let page: Int
}
struct Pager {
private(set) var generation = 0
private(set) var nextPage = 1
private(set) var ids: [Int] = []
private(set) var inFlight: Ticket?
private(set) var hasMore = true
mutating func start() -> Ticket? {
guard hasMore, inFlight == nil else { return nil }
let ticket = Ticket(generation: generation, page: nextPage)
inFlight = ticket
return ticket
}
mutating func reset() {
generation += 1
nextPage = 1
ids = []
inFlight = nil
hasMore = true
}
mutating func finish(
_ ticket: Ticket,
rows: [Int],
hasMore: Bool
) {
guard inFlight == ticket else { return }
var seen = Set(ids)
for id in rows where seen.insert(id).inserted {
ids.append(id)
}
nextPage += 1
self.hasMore = hasMore
inFlight = nil
}
}
var pager = Pager()
let page1 = pager.start()!
assert(pager.start() == nil)
pager.finish(page1, rows: [1, 2], hasMore: true)
let page2 = pager.start()!
pager.finish(page2, rows: [2, 3], hasMore: true)
pager.finish(page2, rows: [2, 3], hasMore: true)
assert(pager.ids == [1, 2, 3])
let oldPage = pager.start()!
pager.reset()
let newPage = pager.start()!
pager.finish(oldPage, rows: [4], hasMore: false)
assert(pager.ids.isEmpty)
pager.finish(newPage, rows: [7], hasMore: false)
assert(pager.ids == [7])
assert(pager.start() == nil)
La première assertion vérifie que plusieurs arrivées consécutives en bas de liste ne déclenchent qu’un seul chargement. Le deuxième groupe couvre à la fois un identifiant présent sur plusieurs pages et l’exécution répétée d’un même callback : l’élément 2 n’apparaît qu’une fois, et une page déjà traitée ne peut plus être acceptée. Le dernier groupe simule un changement de filtre. La page de l’ancienne génération arrive en retard : elle ne doit modifier ni la nouvelle liste ni son état hasMore.
Préserver la validation avant toute écriture lors de l’intégration réseau
La couche d’interface se contente d’émettre des événements « charger la page suivante » ou « réinitialiser la requête ». Au démarrage de la requête réseau, conservez le jeton renvoyé par start() ; à la réception de la réponse, transmettez-la à finish, puis lisez la liste dans le modèle. Vous pouvez également annuler l’ancienne tâche réseau lors d’une réinitialisation, mais son callback de fin ne doit jamais contourner le modèle pour ajouter directement des éléments au tableau de l’interface.
Deux points sont à adapter à l’API. Premièrement, l’exemple déduplique les éléments par ID. Si l’API ne fournit pas d’identifiant stable, définissez d’abord une règle permettant de reconnaître un même enregistrement ; la position dans le tableau ne suffit pas. Deuxièmement, l’exemple avance par numéro de page. Avec une API à curseur, placez dans le jeton le curseur utilisé pour la requête, puis n’enregistrez le nouveau curseur renvoyé par la réponse qu’après avoir accepté celle-ci. Si le nouveau curseur est identique à l’ancien alors que l’API annonce encore une page suivante, consignez une erreur de protocole et arrêtez le chargement automatique pour éviter de redemander indéfiniment la même page.
Où placer la gestion des erreurs et les nouvelles tentatives
L’exemple ne traite que les réponses réussies. Dans une implémentation réelle, la gestion des échecs doit elle aussi vérifier le jeton : l’erreur d’une ancienne requête ne doit pas remplacer l’état de chargement de la nouvelle. Si la page courante échoue, effacez son indicateur de requête en cours sans avancer le numéro de page ; une nouvelle tentative demandée par l’utilisateur doit porter sur cette même page. Présentez distinctement dans l’interface le chargement, l’échec et la possibilité de réessayer, sans vider les éléments déjà affichés à cause d’un simple délai d’attente dépassé.
Exécuter les tests dans un environnement stable
Exécutez d’abord le modèle ci-dessus sur la machine de développement pour valider les règles d’état, puis reprenez les mêmes scénarios dans les tests du projet. C’est le double de test, piloté par le code de test, qui doit décider quand livrer la réponse de chaque page ; de courts délais artificiels ne permettent pas de garantir l’ordre des réponses. Vérifiez ensuite l’interface dans le simulateur iOS : l’indicateur de chargement ne doit apparaître qu’une fois lors d’arrivées répétées en bas de liste, les anciens éléments ne doivent pas réapparaître brièvement après un changement de filtre, et la dernière page ne doit déclencher aucune nouvelle requête.
La latence d’un bureau à distance peut aider à observer le problème, mais elle ne doit pas être une condition de réussite du test. L’intérêt d’un Mac dans le cloud est de permettre à l’équipe de réexécuter ces assertions avec la même chaîne d’outils et le même environnement de simulation ; l’ordre des réponses reste contrôlé par les tests. À chaque modification de la liste, conservez quatre groupes de cas : arrivées répétées en bas de liste, doublons entre pages, ancienne réponse tardive et nouvelle tentative après un échec.
Vérifier tous les chemins d’écriture avant la fusion
Pour finir, parcourez la chaîne d’appels afin de repérer tous les endroits où le tableau est modifié. Examinez les callbacks de fin de requête, l’actualisation par glissement, les changements de filtre et la restauration de l’écran : tous doivent passer par la même vérification du jeton. Sinon, les tests du modèle peuvent réussir alors qu’un callback parallèle continue de polluer la liste. Vérifiez aussi que la déduplication ne change pas silencieusement l’ordre fourni par le serveur et que l’état « dernière page » provient d’une réponse acceptée, non de la première requête arrivée.
Nul besoin d’attendre un réseau dégradé pour rencontrer une course de pagination. Dès que la règle déterminant « quelle réponse a le droit de modifier la liste » devient exécutable, les réponses désordonnées se testent comme n’importe quelle régression, au lieu de rester un défaut occasionnel signalé par une capture d’écran en production.
Questions fréquentes
Annuler l’ancienne requête suffit-il à protéger la liste ?
Non. Avant toute écriture, comparez aussi la génération de la recherche et la page attendue avec celles de la réponse.
Comment traiter un élément présent sur deux pages ?
Dédupliquez-le avec un identifiant métier stable, puis vérifiez séparément que la page ou le curseur progresse.
Préparez un Mac pour votre prochain projet de développement à distance
Choisissez un modèle et un emplacement selon vos besoins, puis connectez-vous via le partage d’écran, VNC ou SSH. Les prix et disponibilités réels sont ceux indiqués en temps réel dans la console.