View in English

  • Global Nav Open Menu Global Nav Close Menu
  • Apple Developer
Search
Cancel
  • Apple Developer
  • News
  • Discover
  • Design
  • Develop
  • Distribute
  • Support
  • Account
Only search within “”

Quick Links

5 Quick Links

Vidéos

Ouvrir le menu Fermer le menu
  • Collections
  • Sujets
  • Toutes les vidéos
  • À propos

Plus de vidéos

  • À propos
  • Résumé
  • Transcription
  • Code
  • Découvrir la simultanéité dans SwiftUI

    Découvrez comment SwiftUI exploite la simultanéité Swift pour créer des apps sûres et réactives. Découvrez comment SwiftUI utilise le main actor par défaut et décharge le travail à d'autres acteurs. Apprenez à interpréter les annotations de simultanéité et à gérer les tâches asynchrones avec la boucle d'évènements de SwiftUI pour des animations fluides et des mises à jour de l'interface utilisateur. Vous repartirez en sachant comment éviter les concurrences de données et écrire du code sans crainte.

    Chapitres

    • 0:00 - Introduction
    • 2:13 - Le main actor Meadows
    • 7:17 - Falaises de concurrence
    • 16:53 - Code Camp
    • 23:47 - Étapes suivantes

    Ressources

    • Concurrency
    • Mutex
    • The Swift Programming Language: Concurrency
    • Updating an App to Use Swift Concurrency
      • Vidéo HD
      • Vidéo SD

    Vidéos connexes

    WWDC25

    • Adoptez la concurrence avec Swift
    • Code-along : Améliorez une app avec la concurrence Swift

    WWDC23

    • Explore SwiftUI animation
  • Rechercher dans cette vidéo…

    Bonjour à tous. Je suis Daniel, de l’équipe SwiftUI.

    Ensemble, nous allons explorer la concurrence et le développement d’apps SwiftUI.

    Vous êtes ici parce que vous avez entendu parler de « data race », ou « courses de données ».

    Peut-être en avez-vous même fait l’expérience.

    Pensez états d’app inattendus, animations instables, voire pertes de données permanentes.

    Mais pas d’inquiétude : vous êtes en sécurité. Parce qu’avec Swift et SwiftUI, ces courses de données n’ont aucune chance. SwiftUI exécute le code simultanément de diverses manières. Vous apprendrez ici à identifier ces scénarios à l’aide des annotations d’accès concurrent des API SwiftUI. L’objectif est que vous sortiez de cette présentation prêt à développer vos apps SwiftUI sans crainte.

    Swift 6.2 inclut un nouveau mode de langage, qui ajoute implicitement l’annotation @MainActor à tous les types d’un module.

    Tout ce que nous verrons ici s’applique indépendamment de ce nouveau mode. Cette présentation repose sur trois axes.

    Nous commencerons par l’annotation MainActor et verrons comment SwiftUI l’applique par défaut lors de la compilation et de l’exécution des applications.

    Nous poursuivrons avec la concurrence et découvrirons comment SwiftUI permet aux apps d’éviter les problèmes d’interface utilisateur en déchargeant le travail du thread principal et en nous protégeant contre les bogues liés à aux courses de données.

    Enfin, nous conclurons en faisant le point sur la relation entre le code concurrent et les API SwiftUI.

    Commençons par l’annotation MainActor.

    Je veux collecter des combinaisons de couleurs inspirées de la nature. J’ai donc créé une app à cet effet. Après avoir pris une photo, je peux choisir le nombre de couleurs souhaité et appuyer sur le bouton Extraire. L’app choisit des couleurs complémentaires sur la photo et les affiche à l’écran.

    Un défilement vers le bas me permet de voir toutes les combinaisons de couleurs extraites et d’exporter ma préférée.

    Pour l’interface utilisateur d’extraction, j’ai créé une struct ColorExtractorView.

    Elle est conforme au protocole d’affichage de SwiftUI, qui déclare l’isolement @MainActor.

    Swift utilise l’isolement des données pour comprendre et valider la sécurité de tous les états mutables. Nous verrons dans cette présentation de nombreux concepts de concurrence comme celui-ci. Pour découvrir la concurrence Swift ou pour rafraîchir vos connaissances, regardez la session sur l’adoption de la concurrence Swift. Dans SwiftUI, View est isolé sur @MainActor et je conforme ma structure à View.

    Par conséquent, ColorExtractorView devient isolé sur @MainActor. Cette ligne pointillée indique l’isolement déduit. Cette annotation est donc implicite au moment de la compilation, mais ne fait pas partie du code que j’ai écrit.

    Le fait que le type global soit isolé sur @MainActor signifie que tous ses membres sont implicitement isolés.

    Cela inclut la propriété body qui implémente l’exigence de View, ainsi que les autres membres que je déclare, comme cette variable @State.

    En terminant sur le corps de la vue, je fais référence à d’autres propriétés membres, comme le schéma du modèle ou une liaison au colorCount du modèle.

    Ceci est autorisé par le compilateur, car l’isolement @MainActor partagé garantit la sécurité de ces accès.

    Cela semble également intuitif.

    @MainActor est l’annotation par défaut de SwiftUI au moment de la compilation. La plupart du temps, je peux donc me concentrer sur la création des fonctionnalités de mon app sans trop avoir à penser à la concurrence.

    Je n’ai pas besoin d’annoter le code à des fins de concurrence. C’est automatiquement sûr.

    Pour faire de la place pour plus de code, je vais juste masquer ces isolements déduits.

    L’ajout de @MainActor par défaut au moment de la compilation va au-delà du code synchrone dans ma vue.

    Les types de mon modèle de données n’ont pas besoin d’annotations @MainActor.

    Comme j’instancie le modèle dans la déclaration de la vue, Swift s’assure que l’instance du modèle est correctement isolée.

    Ce SchemeContentView a un geste de tapotement qui lance le travail d’extraction des couleurs.

    La fonction d’extraction des couleurs est asynchrone. J’utilise donc une tâche pour passer à un contexte asynchrone, afin de l’appeler.

    Comme le corps de la vue est isolé avec @MainActor, la fermeture que j’ai donnée à cette tâche s’exécute aussi sur le thread principal, ce qui est bien pratique.

    L’isolement avec @MainActor est l’action par défaut de SwiftUI au moment de la compilation.

    Cela rend l’écriture des vues pratique et accessible. Mais il existe une autre raison très pratique à cela. Les API d’AppKit et d’UIKit sont exclusivement isolées avec @MainActor.

    SwiftUI interagit de manière transparente avec ces frameworks. Par exemple, le protocole UIViewRepresentable affine le protocole View. Comme avec une structure, cela isole UIViewRepresentable sur @MainActor.

    Un type conforme à UIViewRepresentable est aussi une vue. Il est donc isolé avec @MainActor.

    L’initialiseur d’UILabel nécessite l’isolement avec @MainActor. Cela fonctionne dans mon instance makeUIView, car c’est un membre de mon type représentable isolé avec @MainActor.

    Il n’est pas nécessaire de l’annoter avec @MainActor. SwiftUI annote ses API avec @MainActor, car cela reflète le comportement d’exécution par défaut qu’il implémente.

    Ces annotations sont en aval de la sémantique prévue du framework lors de l’exécution.

    Les annotations d’accès concurrent de SwiftUI expriment sa sémantique d’exécution. Cette distinction peut sembler subtile par rapport aux aspects de compilation que nous avons vus précédemment, mais elle est fondamentale. Nous allons voir un autre exemple qui renforce cette idée.

    Cette prochaine section est particulièrement intéressante. C’est parti !

    Lorsque vous ajoutez des fonctionnalités pendant le développement de l’app, si le thread principal a trop à faire, les images de l’app peut commencer à être saccadées ou à se figer. Vous pouvez utiliser des tâches et une concurrence structurée pour décharger le calcul du thread principal. La vidéo « Elevate an app with Swift Concurrency » propose une série de pratiques pour améliorer les performances de votre app. Assurez-vous de la regarder.

    Cette présentation porte sur la façon dont SwiftUI exploite la concurrence Swift pour améliorer les performances de vos apps.

    Dans le passé, l’équipe de SwiftUI a révélé que les animations intégrées utilisent un thread d’arrière-plan pour calculer leurs états intermédiaires.

    Voyons cela de plus près en examinant ce cercle dans mon SchemeContentView.

    Lorsque l’extraction des couleurs commence et se termine, le cercle s’agrandit, puis revient à sa taille d’origine avec l’animation.

    Pour cela, j’utilise un scaleEffect qui réagit à la propriété isLoading.

    Chaque image de cette animation nécessite une valeur d’échelle distincte comprise entre 1 et 1,5.

    Les valeurs animées comme cette échelle impliquent des mathématiques complexes. S’il y en a beaucoup, leur calcul, image par image, peut être coûteux. C’est pourquoi SwiftUI effectue ce calcul sur un thread d’arrière-plan. Le thread principal a ainsi plus de capacité pour d’autres choses.

    Cette optimisation s’applique également aux API que vous implémentez.

    Et oui ! Parfois, SwiftUI exécute le code en dehors du thread principal.

    Mais pas d’inquiétude : ce n’est pas si compliqué.

    SwiftUI est déclaratif. Contrairement à un UIView, la structure qui se conforme au protocole View n’est pas un objet qui doit occuper un emplacement fixe en mémoire.

    Au moment de l’exécution, SwiftUI crée une représentation distincte pour la vue.

    Cette représentation permet de nombreux types d’optimisations. L’une d’elles consiste à évaluer des parties de la représentation de la vue sur un thread d’arrière-plan.

    SwiftUI réserve cette technique aux cas où une grande partie du calcul est effectuée pour vous. Par exemple, la plupart du temps, il s’agit de calculs de géométrie à haute fréquence.

    Le protocole Shape en est un exemple.

    Ce protocole nécessite une méthode qui renvoie un chemin.

    J’ai créé une forme de coin personnalisée pour représenter une couleur extraite dans ma roue.

    Elle implémente cette méthode de chemin.

    Chaque coin a une orientation distincte. Pendant que la forme du coin s’anime, ma méthode de chemin reçoit les appels d’un thread d’arrière-plan.

    Un autre type de logique personnalisée que SwiftUI exécute pour vous est un argument de fermeture.

    Au milieu du cercle se trouvent ces textes flous.

    Pour obtenir cet effet, j’utilise un visualEffect sur un texte SwiftUI.

    Il modifie le rayon de flou entre deux valeurs lorsque la valeur pulse bascule entre true et false. Le modificateur de vue visualEffect utilise une fermeture pour définir les effets sur la vue du sujet, c’est-à-dire le texte. Le rendu des effets visuels peut être sophistiqué et coûteux.

    SwiftUI peut donc choisir d’appeler cette fermeture à partir d’un thread d’arrière-plan.

    Deux API pourraient donc appeler le code à partir d’un thread d’arrière-plan.

    Examinons-en d’autres.

    Le protocole Layout peut appeler ses méthodes d’exigence hors du thread principal. Comme pour visualEffect, le premier argument d’onGeometryChange est une fermeture qui peut aussi être appelée à partir du thread d’arrière-plan.

    Cette optimisation de l’exécution avec un thread d’arrière-plan fait partie de SwiftUI depuis longtemps.

    SwiftUI peut exprimer ce comportement d’exécution, aussi appelé sémantique, au compilateur et à vous avec l’annotation Sendable.

    Là aussi, les annotations d’accès concurrent de SwiftUI expriment sa sémantique d’exécution.

    L’exécution du code sur un thread séparé libère le thread principal, ce qui rend votre app plus réactive.

    Le mot-clé Sendable est là pour vous rappeler les conditions potentielles de concurrence des données lorsque vous devez partager des données à partir de @MainActor.

    Assimilez Sendable à un panneau d’avertissement sur un sentier qui dit « Danger ! Ne pas courir ! » Certes, cette description est peut-être un peu exagérée. Dans la pratique, Swift détecte toutes les conditions de concurrence potentielles dans le code et vous les rappelle avec des erreurs de compilateur. La meilleure stratégie pour éviter les conditions de course de données est de ne pas partager les données entre les tâches simultanées.

    Lorsqu’une API SwiftUI vous demande d’écrire une fonction sendable, le framework fournit la plupart des variables dont vous avez besoin comme arguments de fonction. Voici un exemple rapide.

    Il y a un détail dans ColorExtactorView que je n’ai pas encore montré. La roue chromatique et le curseur ont la même largeur, grâce à ce type EqualWidthVStack.

    EqualWidthVStack est une mise en page personnalisée.

    Mais ce n’est pas là notre propos. Le fait est que je peux faire tous ces calculs sophistiqués avec l’argument que SwiftUI transmet, sans toucher à aucune variable externe.

    Mais qu’en est-il si je dois accéder à certaines variables en dehors d’une fonction sendable ?

    Dans SchemeContentView, j’ai besoin de l’état pulse dans ce visualEffect.

    Mais Swift dit qu’il existe une condition potentielle de course de données.

    Voyons cela de plus près en zoomant sur ce que l’erreur du compilateur nous dit.

    La variable pulse est l’abréviation de self.pulse. Ce scénario est courant lors du partage d’une variable isolée avec @MainActor dans les fermetures sendable.

    Self est une vue. Elle est isolée sur le Main Actor. C’est notre point de départ. À partir de là, notre objectif final est d’accéder à la variable pulse dans une fermeture sendable. Pour y parvenir, deux choses doivent se produire.

    D’abord, la valeur self doit traverser la limite du Main Actor et rejoindre la région de code des threads d’arrière-plan.

    Dans Swift, nous appelons cela l’envoi de la variable self dans le thread d’arrière-plan.

    Le type de valeur self doit être Sendable.

    Maintenant que self apparaît au bon endroit, lisons sa propriété pulse dans cette région non isolée. Le compilateur ne nous permet pas de le faire à moins que la propriété pulse soit isolée des acteurs.

    En regardant à nouveau le code, comme la valeur self est une vue, elle est protégée par @MainActor.

    Le compilateur la considère donc comme Sendable.

    C’est pourquoi Swift accepte que cette référence à self passe de son isolement avec @MainActor à la fermeture Sendable.

    Swift nous met en fait en garde contre la tentative d’accès à la propriété pulse.

    Nous savons qu’en tant que membre de la vue, pulse est isolé avec @MainActor.

    Le compilateur m’avertit donc que même si je peux ici envoyer self, l’accès à sa propriété pulse isolée avec @MainActor n’est pas sûr.

    Pour corriger cette erreur de compilation, je peux éviter de lire la propriété via une référence à la vue.

    L’effet visuel que j’écris n’a pas besoin de toute la valeur de cette vue. Il veut juste savoir si pulse est true ou false. Je peux copier la variable pulse dans la liste de capture de la fermeture et me référer à cette copie à la place. De cette façon, je n’envoie plus self dans cette fermeture.

    J’envoie une copie de pulse, qui est sendable, car Bool est un type de valeur simple.

    Cette copie n’existe que dans le cadre de cette fonction. Y accéder ici ne pose donc aucun problème de course de données.

    Dans cet exemple, nous n’avons pas pu accéder à cette variable pulse dans une fermeture sendable en raison de sa protection par un acteur global.

    Pour pouvoir y accéder, nous devrions nous assurer que tout ce que nous lisons n’est pas isolé.

    Nous voilà arrivés à la dernière section. Penchons-nous maintenant sur l’organisation du code concurrent.

    Les développeurs SwiftUI expérimentés ont peut-être remarqué que la plupart des API de SwiftUI, comme le rappel d’action du bouton, sont synchrones.

    Pour appeler le code concurrent, passez d’abord à un contexte asynchrone avec une tâche.

    Pourquoi le bouton n’accepte-t-il pas une fermeture asynchrone à la place ?

    Les mises à jour synchrones sont essentielles à une bonne expérience utilisateur. Cela l’est encore plus si les tâches de l’app sont longues et que les gens doivent attendre les résultats.

    Avant de lancer une longue tâche avec une fonction asynchrone, mettez à jour l’interface utilisateur pour indiquer que la tâche est en cours.

    Cette mise à jour doit être synchrone, surtout si elle doit déclencher une animation urgente.

    Imaginons que je demande à un modèle de langage de m’aider à extraire les couleurs. Ce processus d’extraction prend du temps. Dans mon app, j’utilise donc withAnimation pour déclencher de manière synchrone divers états de chargement.

    À la fin de la tâche, j’inverse ensuite ces états de chargement via un autre changement d’état synchrone.

    Les rappels d’action de SwiftUI acceptent les fermetures synchrones, qui sont nécessaires pour configurer les mises à jour de l’interface utilisateur, comme mes états de chargement.

    Les fonctions asynchrones, en revanche, nécessitent une attention particulière, surtout avec les animations. Voyons cela de plus près.

    Dans mon app, un défilement vers le haut montre l’historique des schémas de couleurs de tout à l’heure. Comme chaque schéma apparaît à l’écran, je veux que ses couleurs se révèlent avec une animation.

    Le modificateur d’affichage onScrollVisibilityChange indique l’événement lorsque le jeu de couleurs s’affiche. Dès que cela se produit, je définis une variable d’état sur true pour déclencher l’animation. Cela met à jour le décalage Y de chaque couleur avec l’animation.

    En tant que framework d’interface utilisateur, pour créer des interactions fluides à chaque image, SwiftUI doit tenir compte de la fréquence d’actualisation de l’écran des appareils.

    Cette information est essentielle lorsque je veux que mon code réagisse à un geste continu comme le défilement. Mettons ce code sur la chronologie.

    Je vais utiliser ce triangle vert pour marquer le moment où SwiftUI appelle onScrollVisibilityChange. Le cercle bleu marquera le moment où je déclencherai l’animation avec une mutation d’état.

    Avec cette configuration, le fait que cette mutation ait lieu dans la même image que le rappel de geste peut avoir une grande importance visuelle.

    Supposons que je veuille ajouter un travail asynchrone avant ma mutation animée. Je marque le moment où le travail asynchrone commencera par une ligne orange avec l’opérateur await.

    Dans Swift, appliquer await à une fonction asynchrone crée un point de suspension.

    Une tâche accepte une fonction asynchrone comme argument.

    Lorsque le compilateur voit un opérateur await, il divise la fonction asynchrone en deux.

    Après l’exécution de la première partie, le runtime Swift peut suspendre cette fonction et effectuer d’autres tâches sur le CPU. Cela peut durer plus ou moins longtemps. Puis, le runtime reprend sur la fonction asynchrone d’origine et exécute la seconde partie.

    Ce processus peut se répéter pour chaque occurrence d’await dans la fonction.

    Pour en revenir à la chronologie, cette suspension peut signifier que la fermeture de ma tâche ne reprendra que beaucoup plus tard, dépassant le délai d’actualisation défini par l’appareil.

    Pour l’utilisateur, cela signifie que l’animation semblera lente et saccadée. Une mutation dans une fonction asynchrone ne vous aide pas forcément à atteindre votre objectif.

    SwiftUI fournit des rappels synchrones par défaut. Cela permet d’éviter la suspension involontaire du code asynchrone. La mise à jour correcte de l’interface utilisateur dans les fermetures d’actions synchrones est facile. Vous avez toujours la possibilité d’utiliser une tâche pour activer un contexte asynchrone.

    Les logiques temporelles, comme l’animation, nécessitent que les entrées et sorties de SwiftUI soient synchrones. Les mutations synchrones des propriétés observables et les rappels synchrones sont les types d’interaction les plus naturels avec le framework. Une bonne expérience utilisateur n’implique pas forcément une vaste logique simultanée personnalisée. Le code synchrone est un excellent point de départ et point de terminaison pour de nombreuses apps.

    D’un autre côté, si votre app effectue beaucoup de tâches simultanées, essayez de trouver les limites entre le code de la UI et le code non lié à la UI.

    Il est préférable de séparer la logique des tâches asynchrones de la logique des vues.

    Vous pouvez utiliser un état comme pont. L’état dissocie le code de l’interface utilisateur du code asynchrone.

    Il peut lancer les tâches asynchrones.

    À la fin d’une tâche asynchrone, effectuez une mutation synchrone sur l’état afin que l’interface utilisateur puisse se mettre à jour en conséquence.

    De cette façon, la logique de la UI sera principalement synchrone.

    En prime, il vous sera plus facile d’écrire des tests pour le code asynchrone, car il est désormais indépendant de la logique de l’interface utilisateur.

    Votre vue peut toujours utiliser une tâche pour passer à un contexte asynchrone.

    Mais faites en sorte que le code dans ce contexte asynchrone reste simple. Il est là pour informer le modèle d’un événement d’interface utilisateur.

    Trouver les limites entre le code de l’interface utilisateur qui nécessite de nombreuses modifications temporelles et la logique asynchrone de longue durée permet d’améliorer la structure d’une app.

    Les vues restent ainsi plus facilement synchrones et réactives. Vous devez aussi bien organiser le code non lié à l’interface utilisateur.

    Vous avez une plus grande liberté de le faire avec les conseils que je vous ai montrés.

    Swift 6.2 inclut un paramètre très utile d’isolement des acteurs par défaut. Si vous avez déjà une app, testez-le. Vous pourrez supprimer la plupart de vos annotations @MainActor.

    Mutex est un outil clé pour rendre une classe sendable. Consultez sa documentation officielle pour en savoir plus.

    Mettez-vous au défi d’écrire des tests unitaires pour le code asynchrone de votre app. Essayez de le faire sans importer SwiftUI.

    Et voilà ! C’est ainsi que SwiftUI exploite la concurrence Swift pour vous aider à créer des apps rapides sans course de données.

    J’espère que vous avez bien compris le fonctionnement de la concurrence dans SwiftUI.

    Merci de votre attention.

    • 2:45 - UI for extracting colors

      // UI for extracting colors
      
      struct ColorScheme: Identifiable, Hashable {
          var id = UUID()
          let imageName: String
          var colors: [Color]
      }
      
      @Observable
      final class ColorExtractor {
          var imageName: String
          var scheme: ColorScheme?
          var isExtracting: Bool = false
          var colorCount: Float = 5
      
          func extractColorScheme() async {}
      }
      
      struct ColorExtractorView: View {
          @State private var model = ColorExtractor()
      
          var body: some View {
                  ImageView(
                      imageName: model.imageName,
                      isLoading: model.isExtracting
                  )
                  EqualWidthVStack {
                      ColorSchemeView(
                          isLoading: model.isExtracting,
                          colorScheme: model.scheme,
                          extractCount: Int(model.colorCount)
                      )
                      .onTapGesture {
                          guard !model.isExtracting else { return }
                          withAnimation { model.isExtracting = true }
                          Task {
                              await model.extractColorScheme()
                              withAnimation { model.isExtracting = false }
                          }
                      }
                      Slider(value: $model.colorCount, in: 3...10, step: 1)
                          .disabled(model.isExtracting)
                  }
              }
          }
      }
    • 5:55 - AppKit and UIKit require @MainActor: an example

      // AppKit and UIKit require @MainActor
      // Example: UIViewRepresentable
      
      struct FancyUILabel: UIViewRepresentable {
          func makeUIView(context: Context) -> UILabel {
              let label = UILabel()
              // customize the label...
              return label
          }
      }
    • 6:42 - UI for extracting colors

      // UI for extracting colors
      
      struct ColorScheme: Identifiable, Hashable {
          var id = UUID()
          let imageName: String
          var colors: [Color]
      }
      
      @Observable
      final class ColorExtractor {
          var imageName: String
          var scheme: ColorScheme?
          var isExtracting: Bool = false
          var colorCount: Float = 5
      
          func extractColorScheme() async {}
      }
      
      struct ColorExtractorView: View {
          @State private var model = ColorExtractorModel()
      
          var body: some View {
                  ImageView(
                      imageName: model.imageName,
                      isLoading: model.isExtracting
                  )
                  EqualWidthVStack(spacing: 30) {
                      ColorSchemeView(
                          isLoading: model.isExtracting,
                          colorScheme: model.scheme,
                          extractCount: Int(model.colorCount)
                      )
                      .onTapGesture {
                          guard !model.isExtracting else { return }
                          withAnimation { model.isExtracting = true }
                          Task {
                              await model.extractColorScheme()
                              withAnimation { model.isExtracting = false }
                          }
                      }
                      Slider(value: $model.colorCount, in: 3...10, step: 1)
                          .disabled(model.isExtracting)
                  }
              }
          }
      }
    • 8:26 - Animated circle, part of color scheme view

      // Part of color scheme view
      
      struct SchemeContentView: View {
          let isLoading: Bool
          @State private var pulse: Bool = false
      
          var body: some View {
              ZStack {
                  // Color wheel …
      
                  Circle()
                      .scaleEffect(isLoading ? 1.5 : 1)
      
                  VStack {
                      Text(isLoading ? "Please wait" : "Extract")
      
                      if !isLoading {
                          Text("^[\(extractCount) color](inflect: true)")
                      }
                  }
                  .visualEffect { [pulse] content, _ in
                      content
                          .blur(radius: pulse ? 2 : 0)
                  }
                  .onChange(of: isLoading) { _, newValue in
                      withAnimation(newValue ? kPulseAnimation : nil) {
                          pulse = newValue
                      }
                  }
              }
          }
      }
    • 13:10 - UI for extracting colors

      // UI for extracting colors
      
      struct ColorExtractorView: View {
          @State private var model = ColorExtractor()
      
          var body: some View {
                  ImageView(
                      imageName: model.imageName,
                      isLoading: model.isExtracting
                  )
                  EqualWidthVStack {
                      ColorSchemeView(
                          isLoading: model.isExtracting,
                          colorScheme: model.scheme,
                          extractCount: Int(model.colorCount)
                      )
                      .onTapGesture {
                          guard !model.isExtracting else { return }
                          withAnimation { model.isExtracting = true }
                          Task {
                              await model.extractColorScheme()
                              withAnimation { model.isExtracting = false }
                          }
                      }
                      Slider(value: $model.colorCount, in: 3...10, step: 1)
                          .disabled(model.isExtracting)
                  }
              }
          }
      }
    • 13:47 - Part of color scheme view

      // Part of color scheme view
      
      struct SchemeContentView: View {
          let isLoading: Bool
          @State private var pulse: Bool = false
      
          var body: some View {
              ZStack {
                  // Color wheel …
      
                  Circle()
                      .scaleEffect(isLoading ? 1.5 : 1)
      
                  VStack {
                      Text(isLoading ? "Please wait" : "Extract")
      
                      if !isLoading {
                          Text("^[\(extractCount) color](inflect: true)")
                      }
                  }
                  .visualEffect { [pulse] content, _ in
                      content
                          .blur(radius: pulse ? 2 : 0)
                  }
                  .onChange(of: isLoading) { _, newValue in
                      withAnimation(newValue ? kPulseAnimation : nil) {
                          pulse = newValue
                      }
                  }
              }
          }
      }
    • 17:42 - UI for extracting colors

      // UI for extracting colors
      
      struct ColorExtractorView: View {
          @State private var model = ColorExtractor()
      
          var body: some View {
                  ImageView(
                      imageName: model.imageName,
                      isLoading: model.isExtracting
                  )
                  EqualWidthVStack {
                      ColorSchemeView(
                          isLoading: model.isExtracting,
                          colorScheme: model.scheme,
                          extractCount: Int(model.colorCount)
                      )
                      .onTapGesture {
                          guard !model.isExtracting else { return }
                          withAnimation { model.isExtracting = true }
                          Task {
                              await model.extractColorScheme()
                              withAnimation { model.isExtracting = false }
                          }
                      }
                      Slider(value: $model.colorCount, in: 3...10, step: 1)
                          .disabled(model.isExtracting)
                  }
              }
          }
      }
    • 18:55 - Animate colors as they appear by scrolling

      // Animate colors as they appear by scrolling
      
      struct SchemeHistoryItemView: View {
          let scheme: ColorScheme
          @State private var isShown: Bool = false
      
          var body: some View {
              HStack(spacing: 0) {
                  ForEach(scheme.colors) { color in
                      color
                          .offset(x: 0, y: isShown ? 0 : 60)
                  }
              }
              .onScrollVisibilityChange(threshold: 0.9) {
                  guard !isShown else { return }
                  withAnimation {
                      isShown = $0
                  }
              }
          }
      }
    • 0:00 - Introduction
    • SwiftUI utilise la simultanéité Swift pour aider les développeurs à créer des apps rapides sans conflit de données. Swift 6.2 inclut un nouveau mode de langage qui ajoute implicitement l’annotation @MainActor à tous les types d’un module. SwiftUI exécute le code simultanément de différentes manières et fournit des annotations de simultanéité dans ses API pour aider les développeurs à identifier et à gérer la simultanéité. Cette séance s’attache à comprendre comment SwiftUI gère la simultanéité afin d’éviter les conflits de données et d’améliorer les performances des apps.

    • 2:13 - Le main actor Meadows
    • Le protocole View de SwiftUI est isolé sur @MainActor, ce qui en fait la valeur par défaut du temps de compilation pour le code de l’interface utilisateur. Cela signifie que la majeure partie du code de l’interface utilisateur s’exécute implicitement sur le thread principal, ce qui simplifie le développement et garantit la compatibilité avec UIKit et AppKit. Les modèles de données instanciés dans un protocole View sont également automatiquement isolés. Les annotations @MainActor de SwiftUI reflètent son comportement au moment de l’exécution et sa sémantique prévue, pas seulement les aspects du temps de compilation.

    • 7:17 - Falaises de concurrence
    • SwiftUI utilise des threads d’arrière-plan pour les tâches demandant beaucoup de ressources de calcul, comme les animations et les calculs de formes (par exemple, la méthode « path » du protocole « Shape », la fermeture « visualEffect », le protocole « Layout », la fermeture « onGeometryChange ») afin d’éviter les problèmes d’interface utilisateur. L’annotation « Sendable » signale les conditions potentielles de conflit de données lors du partage de données entre l’acteur principal et les threads d’arrière-plan. Pour éviter ces conflits, minimisez le partage des données. Lorsque le partage est nécessaire, faites des copies des données.

    • 16:53 - Code Camp
    • Les rappels d’actions de SwiftUI sont conçus de manière synchrone pour garantir les mises à jour immédiates de l’interface utilisateur, en particulier pour les animations et les états de chargement. Les tâches de longue durée doivent être lancées de manière asynchrone, mais les mises à jour de l’interface utilisateur doivent rester synchrones. Séparez la logique de l’interface utilisateur de la logique autre (asynchrone), en utilisant l’état comme pont pour déclencher les mises à jour de l’interface utilisateur une fois les tâches asynchrones terminées. Faites en sorte que le code asynchrone de la vue reste simple et attachez-vous à informer le modèle des évènements d’interface utilisateur. La logique temporelle nécessite que les entrées et sorties de SwiftUI soient synchrones.

    • 23:47 - Étapes suivantes
    • Swift 6.2 inclut un paramètre très utile d’isolement des acteurs par défaut. Si vous avez déjà une app, testez-le. Vous pourrez supprimer la plupart de vos annotations @MainActor. Mutex est un outil clé pour rendre une classe « sendable ». Consultez sa documentation officielle pour en savoir plus. Mettez-vous au défi d’écrire des tests unitaires pour le code asynchrone de votre app. Essayez de le faire sans importer SwiftUI.

Developer Footer

  • Vidéos
  • WWDC25
  • Découvrir la simultanéité dans SwiftUI
  • Open Menu Close Menu
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • Icon Composer
    • SF Symbols
    Open Menu Close Menu
    • Accessibility
    • Accessories
    • App Store
    • Audio & Video
    • Augmented Reality
    • Business
    • Design
    • Distribution
    • Education
    • Fonts
    • Games
    • Health & Fitness
    • In-App Purchase
    • Localization
    • Maps & Location
    • Machine Learning & AI
    • Open Source
    • Security
    • Safari & Web
    Open Menu Close Menu
    • Documentation
    • Sample Code
    • Tutorials
    • Downloads
    • Forums
    • Videos
    Open Menu Close Menu
    • Support Articles
    • Contact Us
    • Bug Reporting
    • System Status
    Open Menu Close Menu
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles
    • Feedback Assistant
    Open Menu Close Menu
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program
    • News Partner Program
    • Video Partner Program
    • Security Bounty Program
    • Security Research Device Program
    Open Menu Close Menu
    • Meet with Apple
    • Apple Developer Centers
    • App Store Awards
    • Apple Design Awards
    • Apple Developer Academies
    • WWDC
    Get the Apple Developer app.
    Copyright © 2025 Apple Inc. All rights reserved.
    Terms of Use Privacy Policy Agreements and Guidelines