Back home

Swift Concurrency Series 07|Pièges courants lors de la combinaison de SwiftUI avec async/await

Le véritable piège ne réside généralement pas dans la syntaxe, mais dans la question de savoir si le « cycle de vie de la page » et le « cycle de vie des tâches » sont alignés.

SwiftUI et async/await sont tous deux élégants lorsqu’ils sont vus individuellement, mais lorsqu’ils sont combinés, le problème le plus facilement exposé est le cycle de vie.

Plus précisément, il s’agit de la dislocation entre les deux cycles de vie :

  • Quand une page SwiftUI apparaît, se redessine ou disparaît ?
  • Quand une tâche asynchrone démarre-t-elle, s’interrompt-elle, se termine-t-elle et s’annule-t-elle ?

Si ces deux éléments ne sont pas alignés, même si le code peut s’exécuter aujourd’hui, il grandira facilement plus tard :

  • Répéter la demande
  • Page clignotante
  • Réécrire les anciens résultats
  • L’état de chargement prête à confusion
  • La tâche est toujours mise à jour après avoir quitté la page

Donc, ce dont cet article veut vraiment parler est : les pages asynchrones SwiftUI sont sujettes au chaos, et quelles sont les causes profondes de ce chaos.

1. L’erreur de jugement la plus courante : traiter View comme un objet stable

C’est l’habitude la plus simple à adopter pour de nombreux développeurs ayant une expérience UIKit.

Tout le monde choisira par défaut :

  • La page apparaît une fois
  • Demande à envoyer une fois
  • Mettre à jour la page actuelle après le retour de la demande

Mais View dans SwiftUI ressemble plus à une description d’état qu’à une instance stable qui peut être conservée pendant une longue période.

Cela signifie que si la valeur par défaut dans votre esprit est “la vue devant moi sera toujours là, donc cette tâche lui appartient naturellement”, il sera facile de causer des problèmes plus tard.

SwiftUI n’exige pas que les tâches soient liées à un objet stable, c’est juste qu’il est facile de se tromper en pensant que vous l’avez déjà fait.

2. Le plus gros piège du onAppear est qu’il est utilisé comme entrée d’initialisation unique.

De nombreux articles diront : onAppear peut être exécuté plusieurs fois. Cette affirmation est vraie, mais elle ne suffit pas.

Le vrai danger est qu’il est souvent écrit comme l’entrée standard de facto pour « l’initialisation de la page » :

.onAppear {
  Task {
    await loadData()
  }
}

Le problème n’est pas que ce code doit être faux, mais qu’il est facile d’ajouter mentalement :

“Lorsque cette page apparaîtra, elle ne sera exécutée qu’une seule fois.”

Une fois que vous réfléchissez de cette façon, les éléments suivants apparaîtront l’un après l’autre :

  • Répéter la demande
  • Réinitialiser le statut à plusieurs reprises
  • Enterrer les points à plusieurs reprises
  • Les données ont été effacées juste après leur affichage et redémarrées.

Une idée plus stable est donc : ** Laissez le processus asynchrone lui-même être idempotent, dédupliqué ou remplaçable. **

3. Le deuxième écueil : mélanger le statut de la page et le statut de la tâche

Les états courants dans une page SwiftUI incluent :

  • Critères de filtrage actuels
  • Contenu actuel des données
  • Est-ce que ça charge ?
  • message d’erreur
  • Tâches en cours d’exécution

Si ces états ne sont pas clairement superposés, ils peuvent facilement être regroupés.

Les mauvaises odeurs les plus courantes sont :

  • items
  • isLoading
  • error
  • isRefreshing
  • keyword
  • selectedTab

Chaque valeur a un sens individuellement, mais il est difficile de dire comment elles sont liées les unes aux autres. Ensuite, la page entrera dans un état très gênant :

  • On dirait qu’il est dans n’importe quel état
  • Mais aucun des états n’exprime vraiment “dans quelle sémantique se trouve la page maintenant”

Dans ce cas, dès que le résultat asynchrone revient, n’importe quel statut peut être modifié et le problème ne sera révélé que tôt ou tard.

4. Le troisième écueil : les anciens résultats sont réécrits dans l’interface utilisateur actuelle

C’est l’un des problèmes les plus fréquents dans les pages asynchrones SwiftUI.

Les scénarios typiques incluent :

  • Les utilisateurs peuvent rapidement changer d’onglet
  • Les mots-clés de recherche changent continuellement
  • Changer à plusieurs reprises les conditions du filtre
  • La page déclenche l’actualisation et le premier chargement l’un après l’autre

En apparence, vous pensez peut-être que vous avez simplement « envoyé plusieurs tâches », mais en fait, le vrai problème est le suivant :

**Bien que l’ancienne tâche soit toujours légalement terminée, elle ne correspond plus à l’état actuel de la page. **

Une fois que les anciens résultats peuvent toujours être écrits dans l’interface utilisateur actuelle, l’apparence que vous voyez est généralement la suivante :

  • La page clignote
  • La liste régresse brusquement
  • L’état de chargement se termine soudainement
  • Un message d’erreur apparaît inexplicablement

Ces phénomènes sont très similaires aux « petits bugs », mais les causes profondes sont très similaires : La validité des résultats des tâches n’est pas gérée.

5. Le quatrième puits : répartir toutes les entrées asynchrones sur la Vue

Si ces entrées apparaissent sur une page en même temps :

  • onAppear { Task { ... } }
  • refreshable { await ... }
  • onChange(of:) { Task { ... } }
  • Cliquez sur le bouton pour ouvrir un autre Task

Ils semblent tous légitimes individuellement, mais pris ensemble, ils deviennent rapidement un problème :

**La relation entre les tâches est hors de contrôle. **

Il devient de plus en plus difficile de répondre :

  • qui est l’entrée principale
  • Qui doit annuler qui ?
  • Qui est qualifié pour modifier l’état d’affichage actuel ?
  • À quelle série de tâches correspond une certaine mise à jour de statut ?

Par conséquent, de nombreuses pages asynchrones SwiftUI ont trop d’entrées, chaque entrée peut déclencher directement des tâches et enfin il n’y a pas de couche de coordination unifiée.

6. Le cinquième écueil : la valeur par défaut est “tant que l’interface utilisateur peut être mise à jour”

SwiftUI cache de nombreux détails de mise à jour de l’interface utilisateur, ce qui donne aux gens l’illusion que tant que je change enfin le statut, la page s’actualisera naturellement.

Mais la vraie question n’est pas « sera-t-il rafraîchi ? mais “s’il est qualifié pour rafraîchir à ce moment-là”.

Par exemple :

  • Le résultat actuel est-il expiré ?
  • La page actuelle est-elle toujours active ?
  • Le statut actuel correspond-il toujours à cette série de tâches ?
  • La modification actuelle doit-elle être effectuée sous la sémantique de l’acteur principal ?

Si ces choses ne sont pas prises au sérieux, la page ne plantera peut-être pas immédiatement, mais elle accumulera lentement beaucoup de « confusion accidentelle ».

7. Une approche plus stable : laissez la vue déclencher l’intention et laissez la couche d’état gérer la tâche

La méthode d’organisation que je préfère est :

  • View est responsable de l’expression de l’intention de l’utilisateur
  • ViewModel ou couche d’état est responsable de la gestion des tâches et des qualifications des résultats
  • La vue consomme uniquement l’état trié

Cela dit, il est préférable que la vue en sache moins sur ces détails :

  • S’il faut annuler les anciennes tâches
  • Quel résultat a expiré
  • Le chargement actuel se charge-t-il pour la première fois ou est-il actualisé ?
  • Si l’état d’erreur doit écraser l’ancien contenu

Si ces problèmes restent sur la vue, la page passera rapidement de « interface utilisateur déclarative » à « shell déclaratif enveloppé dans une couche de logique asynchrone décentralisée ».

8. Une suggestion plus proche du combat réel

Si une page SwiftUI commence à devenir compliquée, je me force généralement à répondre d’abord aux questions suivantes :

  1. Quelles entrées de tâches y a-t-il sur la page ?
  2. Si des tâches similaires coexistent, remplacent ou ignorent.
  3. Quels états sont des états sémantiques de page et lesquels ne sont que des états de processus internes.
  4. Si les anciens résultats peuvent toujours être modifiés vers la page actuelle.
  5. Après avoir quitté la page, quelles tâches doivent être poursuivies et lesquelles doivent être arrêtées.

Une fois que vous ne pouvez pas répondre à ces questions, cela signifie généralement que le modèle de tâche de page n’a pas encore été établi.

9. Conclusion : La vraie difficulté des pages asynchrones SwiftUI est l’alignement du cycle de vie

Une situation courante est que les principaux pièges entre SwiftUI et async/await sont la syntaxe. Mais la situation la plus réelle est la suivante :

  • View n’est pas un objet stable comme on pourrait le penser en surface.
  • onAppear n’est pas une sémantique d’initialisation unique
  • Les anciens résultats n’expireront pas automatiquement car ils ont “expiré”
  • S’il y a trop d’entrées de tâches au niveau de la page, cela va certainement commencer à devenir compliqué.

Ainsi, la page asynchrone SwiftUI vraiment stable est :

Alignez d’abord le cycle de vie de la page avec le cycle de vie des tâches, puis écrivez du code asynchrone spécifique.

Ce n’est que si cela est établi à l’avance que la légèreté de SwiftUI et l’élégance de async/await deviendront véritablement des avantages, plutôt que de faciliter l’écriture du chaos.