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

Retour à WWDC25

  • À propos
  • Transcription
  • Code
  • Code-along : Améliorez une app avec la concurrence Swift

    Découvrez comment optimiser l'expérience utilisateur de votre app avec la concurrence Swift en mettant à jour une app existante. Nous commencerons avec une app basée sur un main actor, puis introduirons progressivement du code asynchrone selon les besoins. Nous utiliserons des tâches pour optimiser le code s'exécutant sur le main actor et découvrirons comment paralléliser le code en déchargeant le travail en arrière-plan. Nous explorerons les avantages de la sécurité contre les conflits d'accès aux données et apprendrons à interpréter et à corriger les erreurs liées à cette sécurité. Enfin, nous montrerons comment tirer le meilleur parti de la concurrence structurée dans le contexte d'une app.

    Chapitres

    • 0:00 - Introduction
    • 2:11 - Configuration accessible de la concurrence
    • 2:51 - Architecture d’exemple d’app
    • 3:42 - Chargement asynchrone de photos depuis la photothèque
    • 9:03 - Extraction de l’autocollant et des couleurs de la photo
    • 12:30 - Exécution de tâches sur un thread d’arrière-plan
    • 15:58 - Parallélisation des tâches
    • 18:44 - Prévenir les conflits d’accès aux données avec Swift 6
    • 27:56 - Maîtriser le code asynchrone avec la concurrence structurée
    • 31:36 - Conclusion

    Ressources

    • Code-along: Elevating an app with Swift concurrency
    • Swift Migration Guide
      • Vidéo HD
      • Vidéo SD

    Vidéos connexes

    WWDC25

    • Adoptez la concurrence avec Swift
    • Découvrir la simultanéité dans SwiftUI

    WWDC23

    • Analyze hangs with Instruments
    • Beyond the basics of structured concurrency
  • Rechercher dans cette vidéo…

    Bonjour. Je m’appelle Sima et je travaille sur Swift et SwiftUI. Dans cette vidéo, vous apprendrez à améliorer votre app avec la concurrence Swift. En tant que développeurs d’apps, nous écrivons la majeure partie du code sur le thread principal.

    Le code monothread est facile à comprendre et à gérer. Mais en même temps, une app moderne doit souvent effectuer des tâches chronophages, comme une requête réseau ou des calculs coûteux. En pareil cas, il est conseillé de déplacer le travail hors du thread principal pour que l’app reste réactive. Swift vous offre tous les outils requis pour écrire du code concurrent en toute confiance. Pendant cette séance, nous verrons comment faire en créant une app avec vous. Nous commencerons avec une app basée sur un monothread, puis introduirons progressivement du code asynchrone. Ensuite, nous améliorerons les performances de l’app en déchargeant certaines des tâches coûteuses et en les exécutant en parallèle. Ensuite, nous discuterons de certains scénarios de sécurité comme les conflits de données, et verrons comment les aborder. Enfin, j’aborderai la concurrence structurée et vous montrerai comment utiliser des outils tels que TaskGroup pour mieux contrôler votre code concurrent. J’ai hâte de commencer ! J’adore tenir un journal et décorer mes notes avec des autocollants, aussi je vous expliquerai comment créer une app permettant de créer des séries d’autocollants avec n’importe quel ensemble de photos. Notre app aura deux vues principales. La première présentera un carrousel de tous les autocollants, avec un dégradé des couleurs de la photo d’origine, et la seconde une grille de la série d’autocollants, prête à être exportée. N’hésitez pas à télécharger l’exemple d’app ci-dessous pour suivre ! Lorsque j’ai créé le projet, Xcode a activé quelques fonctionnalités qui offrent une approche plus accessible de la concurrence, notamment le mode main actor par défaut et d’autres. Ces fonctionnalités sont activées par défaut dans les nouveaux projets d’app dans Xcode 26.

    Dans la configuration de familiarisation avec la concurrence, le mode de langage Swift 6 préviendra les conflits de données sans instaurer de concurrence jusqu’à ce que vous soyez prêt. Si vous souhaitez activer ces fonctionnalités dans des projets existants, consultez le guide de migration vers Swift.

    Dans le code, l’app aura deux vues principales : StickerCarousel et StickerGrid. Ces vues utiliseront les autocollants que la struct PhotoProcessor est chargée d’extraire.

    Le PhotoProcessor extrait l’image brute de la photothèque avant de renvoyer l’autocollant.

    La vue StickerGrid dispose d’un ShareLink qu’elle peut utiliser pour partager les autocollants.

    Le type PhotoProcessor effectue deux opérations coûteuses : l’extraction des autocollants et le calcul des couleurs dominantes. Voyons comment les fonctionnalités de concurrence de Swift peuvent nous aider à optimiser l’expérience utilisateur, tout en laissant l’appareil effectuer ces tâches coûteuses ! Commençons par la vue StickerCarousel. Cette vue fait défiler les autocollants dans une vue horizontale. Dans la vue de défilement, un ForEach itère sur le tableau des photos sélectionnées dans la photothèque stockée dans le modèle de vue. Il vérifie le dictionnaire processedPhotos dans le viewModel pour obtenir la photo traitée correspondant à la sélection dans la photothèque. Actuellement, nous n’avons aucune photo traitée, car je n’ai pas écrit de code pour obtenir une image à partir du sélecteur de photos. Si j’exécute l’app maintenant, tout ce que nous verrons dans la vue de défilement, c’est la vue StickerPlaceholder. Je vais accéder à StickerViewModel à l’aide de Commande+clic. Le StickerViewModel stocke un tableau des photos actuellement sélectionnées dans la photothèque, représentées sous le type SelectedPhoto. Je vais ouvrir l’aide rapide avec Option-clic pour en savoir plus sur ce type.

    SelectedPhoto est un type identifiable qui stocke un PhotosPickerItem du framework PhotosUI et son identifiant associé. Le modèle possède également le dictionnaire appelé processedPhotos qui associe l’ID de la photo sélectionnée à l’image SwiftUI qu’elle représente. J’ai déjà commencé à travailler sur la fonction loadPhoto qui prend la photo sélectionnée. Mais actuellement, elle ne charge aucune donnée du sélecteur de photos qu’elle stocke. Le PhotosPickerItem est conforme au protocole Transferable du SDK, ce qui me permet de charger la représentation que je demande avec la fonction asynchrone loadTransferable. Je vais demander la représentation Data.

    Nous avons maintenant une erreur de compilation.

    C’est parce que l’appel à « loadTransferable » est asynchrone, et que ma fonction « loadPhoto » par laquelle je l’appelle n’est pas configurée pour gérer les appels asynchrones. Aussi, Swift m’aide en suggérant de marquer « loadPhoto » avec le mot-clé async. Je vais appliquer cette suggestion.

    Notre fonction est capable de gérer le code asynchrone. Mais, il y a encore une autre erreur. Bien que « loadPhoto » puisse gérer les appels asynchrones, nous devons lui dire ce qu’il doit attendre. Pour ce faire, je dois marquer l’appel à « loadTransferable » avec le mot-clé « await ». Je vais appliquer le correctif suggéré.

    Je vais appeler cette fonction dans la vue StickerCarousel. Avec Commande-Maj-O, je peux utiliser l’option Open Quickly de Xcode pour revenir au StickerCarousel.

    J’aimerais appeler la fonction loadPhoto lorsque la vue StickerPlaceholder apparaît. Comme cette fonction est asynchrone, j’utiliserai le modificateur de tâche SwiftUI pour lancer le traitement des photos lorsque cette vue apparaîtra.

    Vérifions cela sur mon appareil !

    Parfait, cela fonctionne. Essayons de sélectionner quelques photos pour effectuer un test.

    Parfait ! On dirait que les images sont en cours de chargement depuis ma photothèque. Cette tâche me permet de préserver la réactivité de l’interface de l’app pendant le chargement de l’image à partir des données. Et comme j’utilise LazyHStack pour afficher les images, je ne lance les tâches de chargement des photos que pour les vues exigeant un rendu à l’écran, de sorte que l’app n’effectue pas plus de travail que nécessaire. Voyons pourquoi async/await améliore la réactivité de notre app.

    Nous avons ajouté le mot-clé « await » lors de l’appel de la méthode « loadTransferable », et annoté la fonction « loadPhoto » avec « async ». Le mot-clé « await » marque un point de suspension possible. Cela signifie qu’initialement, la fonction loadPhoto démarre sur le thread principal, et lorsqu’elle appelle loadTransferable, elle s’interrompt à l’étape await en attendant que loadTransferable se termine. Lorsque loadPhoto est en pause, le framework Transferable exécute loadTransferable sur le thread d’arrière-plan. Lorsque loadTransferable est terminé, loadPhoto reprend son exécution sur le thread principal et met à jour l’image. Le thread principal est libre de répondre aux événements de l’interface et d’exécuter d’autres tâches pendant que loadPhoto est en pause. Le mot-clé await indique un point dans votre code qui autorise l’exécution d’autres travaux pendant que votre fonction est en pause. Et voilà, nous avons fini de charger les images de la photothèque ! Au cours de l’opération, nous avons appris ce que signifie le code asynchrone, comment l’écrire et l’aborder. Ajoutons maintenant du code à notre app pour extraire l’autocollant de la photo, ainsi que ses couleurs principales que nous pouvons utiliser pour le dégradé d’arrière-plan lors de l’affichage dans une vue carrousel.

    Je vais utiliser la touche Commande+clic pour revenir à loadPhoto où je peux appliquer ces effets.

    Le projet comprend déjà un PhotoProcessor, qui prend les données, extrait les couleurs et l’autocollant, et renvoie la photo traitée. Plutôt que de fournir l’image de base à partir des données, je vais utiliser le PhotoProcessor.

    Le PhotoProcessor renvoie une photo traitée, je vais donc mettre à jour le type de dictionnaire.

    Cette ProcessedPhoto nous fournira l’autocollant extrait de la photo et la gamme de couleurs à partir de laquelle composer le dégradé.

    J’ai déjà inclus une vue GradientSticker dans le projet utilisant une processedPhoto. Je vais utiliser Open Quickly pour y accéder.

    Cette vue montre un autocollant enregistré dans une photo au-dessus d’un dégradé linéaire dans une pile ZStack.

    Je vais ajouter ce GradientSticker dans le carrousel.

    Actuellement, dans le StickerCarousel, nous ne faisons que redimensionner la photo, mais maintenant que nous avons une photo traitée, nous pouvons utiliser le GradientSticker à la place.

    Créons et exécutons l’app pour voir nos autocollants !

    Ça marche !

    Oh non ! Pendant l’extraction des autocollants, le défilement dans le carrousel n’est pas très fluide.

    Je suppose que le traitement d’image est très coûteux. J’ai dressé le profil de l’app à l’aide d’Instruments pour le confirmer. Le tracé indique que notre app présente des blocages importants.

    Si j’effectue un zoom avant et que je regarde la trace de pile la plus lourde, je peux voir le processeur photo bloquer le thread principal avec les tâches de traitement coûteuses pendant plus de 10 secondes ! Si vous souhaitez en savoir plus sur l’analyse des blocages dans votre app, consultez notre séance « Analyze hangs with Instruments ». Parlons maintenant un peu plus du travail effectué par notre app sur le thread principal.

    L’implémentation de « loadTransferable » a permis de décharger le travail en arrière-plan, afin d’éviter que le travail de chargement ne se fasse sur le thread principal.

    Maintenant que nous avons ajouté le code de traitement d’image, qui s’exécute sur le thread principal et prend beaucoup de temps, le thread principal est incapable de recevoir des mises à jour de l’interface, comme répondre aux gestes de défilement, ce qui génère une mauvaise expérience utilisateur dans mon app.

    Auparavant, nous avons adopté une API asynchrone à partir du SDK, ce qui nous déchargeait du travail. Maintenant, nous devons exécuter notre propre code en parallèle pour corriger le blocage. Nous pouvons faire passer certaines transformations d’images en arrière-plan. La transformation d’image se compose de ces trois opérations. L’obtention de l’image brute et la mise à jour de l’image doivent interagir avec l’interface. Nous ne pouvons donc pas déplacer ce travail en arrière-plan, mais nous pouvons décharger le traitement de l’image. Cela permettra au thread principal d’être libre de répondre à d’autres événements pendant le travail de traitement d’image coûteux. Examinons la struct PhotoProcessor pour comprendre comment procéder !

    Comme mon app est en mode main actor par défaut, le PhotoProcessor est lié à @MainActor, ce qui signifie que toutes ses méthodes doivent s’exécuter sur le main actor. La méthode « process » appelle les méthodes extract sticker et extract colors. Je dois donc marquer toutes les méthodes de ce type comme pouvant s’exécuter sur le main actor. Pour ce faire, je peux marquer l’ensemble du type PhotoProcessor avec nonisolated. Il s’agit d’une nouvelle fonctionnalité introduite dans Swift 6.1. Lorsque le type est marqué avec nonisolated, toutes ses propriétés et méthodes sont automatiquement non isolées.

    Maintenant que le PhotoProcessor n’est pas lié au MainActor, nous pouvons appliquer le nouvel attribut « @concurrent » à la fonction process et la marquer avec « async ». Cela indiquera à Swift de toujours basculer vers un thread d’arrière-plan lors de l’exécution de cette méthode. Je vais utiliser Open Quickly pour revenir au PhotoProcessor.

    Tout d’abord, je vais appliquer nonisolated au type, pour dissocier le PhotoProcessor du main actor et permettre à ses méthodes d’être appelées par le code concurrent.

    Maintenant que PhotoProcessor est nonisolated, pour m’assurer que la méthode process sera appelée à partir du thread d’arrière-plan, je vais appliquer @concurrent et async.

    Je reviens à présent au StickerViewModel avec Open Quickly.

    Ici, dans la méthode loadPhoto, je dois sortir du thread principal en appelant la méthode process avec le mot-clé « await », ce que Swift suggère déjà. Je vais appliquer cette suggestion.

    Créons et exécutons notre app pour voir si le fait de déplacer ce travail hors de le main actor résout les problèmes de blocage !

    On dirait qu’il n’y a plus de blocages lors du défilement !

    Mais même si je peux interagir avec l’interface, l’image met un certain temps à apparaître dans l’interface pendant le défilement. La réactivité d’une app n’est pas le seul facteur d’amélioration de l’expérience utilisateur. Si nous déplaçons le travail hors du thread principal, mais qu’il faut beaucoup de temps pour que l’utilisateur obtienne les résultats, l’utilisation de l’app peut rester frustrante.

    Nous avons déplacé l’opération de traitement d’image vers un thread d’arrière-plan, mais cela prend encore du temps. Voyons comment optimiser cette opération avec la concurrence pour qu’elle se termine plus rapidement. Le traitement de l’image nécessite l’extraction des autocollants et des couleurs dominantes (des opérations indépendantes les unes des autres). Nous pouvons donc exécuter ces tâches en parallèle les unes des autres en utilisant async let. Désormais, le pool de threads concurrents, qui gère tous les threads d’arrière-plan, planifiera le démarrage de ces deux tâches sur deux threads d’arrière-plan différents à la fois. Cela me permet de profiter des multiples cœurs de mon téléphone.

    Je vais effectuer un Commande+clic sur la méthode process pour adopter async let.

    En maintenant la touche Ctrl + Maj et la touche fléchée vers le bas enfoncées, je peux utiliser le curseur multiligne pour ajouter async devant les variables sticker et colors.

    Maintenant que nous avons exécuté ces deux appels en parallèle, nous devons attendre leurs résultats pour reprendre notre fonction process. Corrigeons tous ces problèmes à l’aide du menu Éditeur.

    Mais, il y a encore une autre erreur. Cette fois, il s’agit d’une course de données ! Prenons le temps de comprendre cette erreur.

    Cette erreur signifie que mon type PhotoProcessor n’est pas sûr à partager entre des tâches simultanées. Pour comprendre pourquoi, examinons ses propriétés stockées. La seule propriété stockée par le PhotoProcessor est une instance de ColorExtractor, nécessaire pour extraire les couleurs de la photo. La classe ColorExtractor calcule les couleurs dominantes qui apparaissent dans l’image. Ce calcul s’applique à des données d’image mutables de bas niveau, y compris des tampons de pixels, de sorte que le type d’extracteur de couleur n’est pas sûr pour un accès concurrent.

    À l’heure actuelle, toutes les opérations d’extraction de couleur partagent la même instance de ColorExtractor. Cela entraîne un accès concurrent à la même mémoire.

    C’est ce qu’on appelle une « course de données », qui peut générer des bogues d’exécution tels que des plantages et des comportements imprévisibles. Le mode de langage Swift 6 les identifie au moment de la compilation, ce qui élimine cet ensemble de bogues lorsque vous écrivez du code qui s’exécute en parallèle. Cela transforme ce qui aurait pu être un bogue d’exécution délicat en une erreur de compilation que vous pouvez immédiatement corriger. Si vous cliquez sur le bouton « aide » du message d’erreur, vous pouvez en savoir plus sur cette erreur sur le site web de Swift. Plusieurs options sont envisageables lorsque vous essayez de remédier à un conflit de données. Le choix de l’une d’elles dépend de la façon dont votre code utilise les données partagées. Tout d’abord, posez-vous cette question : Cet état modifiable doit-il être partagé entre du code concurrent ? Bien souvent, vous pouvez simplement éviter de le partager. Cependant, il arrive que l’état doive être partagé par un tel code. Dans ce cas, vous pouvez extraire ce que vous devez partager vers un type de valeur sûr à envoyer. Si aucune de ces solutions n’est applicable, vous pouvez envisager d’isoler cet état dans un acteur tel que le MainActor. Voyons si la première solution fonctionnerait dans notre cas. Bien que nous puissions refactoriser ce type pour qu’il fonctionne différemment et gère plusieurs opérations concurrentes, nous allons plutôt déplacer l’extracteur de couleurs vers une variable locale dans la fonction extractColors. Ainsi, chaque photo en cours de traitement aura sa propre instance de l’extracteur de couleurs. Il s’agit de la bonne modification du code, car l’extracteur de couleurs est destiné à travailler sur une seule photo à la fois. Nous voulons donc une instance distincte pour chaque tâche d’extraction de couleur. Avec cette modification, rien en dehors de la fonction extractColors ne peut accéder à l’extracteur de couleurs, ce qui empêche le conflit de données !

    Pour effectuer cette modification, déplaçons la propriété color extractor vers la fonction extractColors.

    Parfait ! Avec l’aide du compilateur, nous avons pu détecter et éliminer un conflit de données dans notre app. Maintenant, exécutons-la !

    Je sens que l’app s’exécute plus vite !

    Si je recueille une trace du profileur dans Instruments et que je l’ouvre, je ne vois plus les blocages. Récapitulons rapidement les optimisations apportées avec la concurrence Swift ! En adoptant l’attribut « @concurrent », nous avons réussi à déplacer notre code de traitement d’image hors du thread principal. Nous avons également mené ses opérations en parallèle, l’extraction des autocollants et des couleurs à l’aide * de « async let », ce qui rend notre app beaucoup plus performante ! Les optimisations effectuées avec la concurrence Swift doivent toujours s’appuyer sur des données provenant d’outils d’analyse, tels que l’instrument Time Profiler. Si vous pouvez rendre votre code plus efficace sans introduire de concurrence, c’est la solution à appliquer en priorité. L’app est rapide maintenant ! Faisons une pause dans le traitement d’image et ajoutons quelque chose d’amusant !

    Ajoutons un effet visuel pour nos autocollants traités qui estompera et floutera l’autocollant lors de son défilement. Passons à Xcode pour écrire cela !

    Je vais revenir au StickerCarousel à l’aide du navigateur de projets Xcode.

    Maintenant, je vais appliquer l’effet visuel sur chaque image de la vue de défilement à l’aide du modificateur visualEffect.

    Ici, j’applique quelques effets sur la vue. Je veux modifier le décalage, le flou et l’opacité uniquement pour le dernier autocollant de la vue de défilement. Je dois donc accéder à la propriété de sélection de viewModel pour vérifier si l’effet visuel est appliqué au dernier autocollant.

    Il semble y avoir une erreur de compilation, car j’essaie d’accéder à l’état de la vue protégée du main actor à partir de la fermeture visualEffect. Le calcul d’un effet visuel étant coûteux, SwiftUI le décharge du thread principal pour optimiser les performances de mon app.

    Si vous vous sentez l’âme d’un aventurier et que vous souhaitez en savoir plus, consultez notre séance « Explore concurrency in SwiftUI ». Voici ce que cette erreur me dit : cette fermeture sera évaluée plus tard en arrière-plan. Confirmons cela en regardant la définition de « visualEffect », sur laquelle je vais appliquer une Commande+clic.

    Dans la définition, cette fermeture est @Sendable. SwiftUI indique ainsi qu’elle sera évaluée en arrière-plan.

    Dans ce cas, SwiftUI appelle à nouveau l’effet visuel chaque fois que la sélection change, afin que je puisse en faire une copie en utilisant la liste de capture de la fermeture.

    Désormais, lorsque SwiftUI appelle cette fermeture, il utilise une copie de la valeur de sélection, ce qui supprime tout conflit de données lors de cette opération.

    Découvrons notre effet visuel !

    Il est parfait, et je peux voir comment l’image précédente s’estompe à mesure que je la fais défiler.

    Dans ces deux scénarios de conflit de données, la solution consistait à ne pas partager les données pouvant être modifiées à partir d’un code concurrent. La principale différence était que dans le premier exemple, j’ai moi-même introduit un conflit de données en exécutant une partie du code en parallèle. Dans le second exemple, cependant, j’ai utilisé une API SwiftUI qui décharge le travail sur le thread d’arrière-plan en mon nom.

    Si vous devez partager un état mutable, il existe d’autres moyens de le protéger. Les types de valeur Sendable empêchent le partage du type par un code concurrent. Par exemple, les méthodes extractSticker et extractColors s’exécutent en parallèle et prennent toutes deux les mêmes données d’image. Toutefois, il n’y a pas de conflit de données dans ce cas, car Data est un type de valeur Sendable. Data implémente également la copie sur écriture, il n’est donc copié que s’il est muté. Si vous ne pouvez pas utiliser un type de valeur, vous pouvez envisager d’isoler votre état vers le main actor. Heureusement, le mode main actor par défaut le fait déjà pour vous. Par exemple, notre modèle est une classe et nous pouvons y accéder à partir d’une tâche simultanée. Le modèle étant implicitement marqué avec le MainActor, on peut sans danger s’y référer à partir d’un code concurrent. Le code devra basculer vers le main actor pour accéder à l’état. Dans ce cas, la classe est protégée par le main actor, mais il en va de même pour les autres acteurs que vous pourriez avoir dans votre code. Notre app a fière allure jusqu’à présent ! Mais elle n’est toujours pas terminée.

    Pour pouvoir exporter les autocollants, ajoutons une grille d’autocollants qui lance une tâche de traitement pour chaque photo qui n’a pas encore été traitée, et affiche tous les autocollants en même temps. Elle aura également un bouton de partage qui permettra d’exporter ces autocollants. Revenons au code !

    Tout d’abord, j’utiliserai la touche Commande+clic pour accéder au StickerViewModel.

    Je vais ajouter une autre méthode à notre modèle, « processAllPhotos() ».

    Ici, je veux itérer sur toutes les photos traitées sauvegardées jusqu’à présent dans mon modèle, et s’il reste des photos non traitées, je veux démarrer plusieurs tâches en parallèle pour les traiter en une fois.

    Nous avons déjà utilisé async let, mais cela n’a fonctionné que parce que nous savions qu’il n’y avait que deux tâches à lancer : l’extraction des autocollants et l’extraction des couleurs. Maintenant, nous devons créer une nouvelle tâche pour toutes les photos brutes du tableau, et le nombre de ces tâches de traitement peut être multiple.

    Les API telles que TaskGroup vous permettent de mieux contrôler le travail asynchrone que votre app doit effectuer.

    Les groupes de tâches offrent un contrôle précis sur les tâches enfants et leurs résultats. Le groupe de tâches permet de lancer n’importe quel nombre de tâches enfants qui peuvent être exécutées en parallèle.

    Chaque tâche enfant peut avoir une durée arbitraire, de sorte qu’elles peuvent être effectuées selon un ordre différent. Dans notre cas, les photos traitées seront enregistrées dans un dictionnaire, donc l’ordre n’a pas d’importance.

    TaskGroup est conforme à AsyncSequence, ce qui nous permet d’itérer sur les résultats au fur et à mesure de leur obtention pour les sauvegarder dans le dictionnaire. Et enfin, nous pouvons attendre que tout le groupe termine les tâches enfants. Revenons au code pour adopter un groupe de tâches ! Pour adopter le groupe de tâches, je vais commencer par le déclarer.

    Ici, à l’intérieur de la fermeture, j’ai une référence au groupe auquel je peux ajouter des tâches de traitement d’image. Je vais itérer sur la sélection sauvegardée dans le modèle.

    Si cette photo a été traitée, je n’ai pas besoin de créer de tâche pour elle.

    Je vais démarrer une nouvelle tâche de chargement des données et de traitement de la photo.

    Étant donné que le groupe est une séquence asynchrone, je peux itérer dessus pour enregistrer la photo traitée dans le dictionnaire processedPhotos une fois qu’elle est prête.

    C’est tout ! Nous sommes maintenant prêts à afficher nos autocollants dans la StickerGrid.

    Je vais utiliser Open Quickly pour naviguer vers la StickerGrid.

    Ici, j’ai une propriété d’état finishedLoading qui indique si le traitement de l’ensemble des photos est fini.

    Si tel n’est pas le cas, une vue de progression s’affiche. Je vais appeler la méthode processAllPhotos() que nous venons d’implémenter.

    Une fois toutes les photos traitées, nous pouvons définir la variable d’état. Et enfin, je vais ajouter le lien de partage dans la barre d’outils pour partager les autocollants !

    Je renseigne les éléments du lien de partage avec un autocollant pour chaque photo sélectionnée.

    Lançons l’app !

    Je vais cliquer sur le bouton StickerGrid. Grâce au TaskGroup, la grille de prévisualisation commence à traiter toutes les photos en même temps. Et lorsqu’elles sont prêtes, je peux voir instantanément tous les autocollants ! Enfin, en utilisant le bouton Partager de la barre d’outils, je peux exporter tous les autocollants sous forme de fichiers que je peux enregistrer.

    Dans notre app, les autocollants seront collectés dans l’ordre de leur traitement. Mais vous pouvez également suivre l’ordre, et le groupe de tâches offre de nombreuses autres possibilités. Pour en savoir plus, consultez la séance « Beyond the basics of structured concurrency ».

    Félicitations ! L’app est terminée et je peux maintenant enregistrer mes autocollants ! Nous avons ajouté de nouvelles fonctionnalités à une app, découvert quand elles avaient un impact sur l’interface et utilisé la concurrence autant que nécessaire pour améliorer la réactivité et les performances. Nous avons également appris ce qu’est la concurrence structurée et comment éviter les courses de données.

    Si vous n’avez pas suivi, vous pouvez toujours télécharger la version finale de l’app et créer des autocollants à partir de vos propres photos ! Pour vous familiariser avec les nouvelles fonctionnalités et techniques de concurrence Swift mentionnées dans cet exposé, essayez d’optimiser ou de peaufiner davantage l’app. Enfin, voyez si vous pouvez appliquer ces techniques à votre app et n’oubliez pas de la profiler d’abord ! Pour mieux comprendre les concepts du modèle de concurrence de Swift, consultez notre séance « Embrace Swift concurrency ». Pour migrer votre projet existant afin d’adopter de nouvelles fonctionnalités de concurrence accessibles, consultez « Swift Migration Guide » ! Et ma partie préférée, j’ai reçu des autocollants pour mon carnet ! Merci de votre attention !

    • 6:29 - Asynchronously loading the selected photo from the photo library

      func loadPhoto(_ item: SelectedPhoto) async {
          var data: Data? = try? await item.loadTransferable(type: Data.self)
      
          if let cachedData = getCachedData(for: item.id) { data = cachedData }
      
          guard let data else { return }
          processedPhotos[item.id] = Image(data: data)
      
          cacheData(item.id, data)
      }
    • 6:59 - Calling an asynchronous function when the SwiftUI View appears

      StickerPlaceholder()
          .task {
              await viewModel.loadPhoto(selectedPhoto)
          }
    • 9:45 - Synchronously extracting the sticker and the colors from a photo

      func loadPhoto(_ item: SelectedPhoto) async {
          var data: Data? = try? await item.loadTransferable(type: Data.self)
      
          if let cachedData = getCachedData(for: item.id) { data = cachedData }
      
          guard let data else { return }
          processedPhotos[item.id] = PhotoProcessor().process(data: data)
      
          cacheData(item.id, data)
      }
    • 9:56 - Storing the processed photo in the dictionary

      var processedPhotos = [SelectedPhoto.ID: ProcessedPhoto]()
    • 10:45 - Displaying the sticker with a gradient background in the carousel

      import SwiftUI
      import PhotosUI
      
      struct StickerCarousel: View {
          @State var viewModel: StickerViewModel
          @State private var sheetPresented: Bool = false
      
          var body: some View {
              ScrollView(.horizontal) {
                  LazyHStack(spacing: 16) {
                      ForEach(viewModel.selection) { selectedPhoto in
                          VStack {
                              if let processedPhoto = viewModel.processedPhotos[selectedPhoto.id] {
                                  GradientSticker(processedPhoto: processedPhoto)
                              } else if viewModel.invalidPhotos.contains(selectedPhoto.id) {
                                  InvalidStickerPlaceholder()
                              } else {
                                  StickerPlaceholder()
                                      .task {
                                          await viewModel.loadPhoto(selectedPhoto)
                                      }
                              }
                          }
                          .containerRelativeFrame(.horizontal)
                      }
                  }
              }
              .configureCarousel(
                  viewModel,
                  sheetPresented: $sheetPresented
              )
              .sheet(isPresented: $sheetPresented) {
                  StickerGrid(viewModel: viewModel)
              }
          }
      }
    • 14:13 - Allowing photo processing to run on the background thread

      nonisolated struct PhotoProcessor {
       
          let colorExtractor = ColorExtractor()
      
          @concurrent
          func process(data: Data) async -> ProcessedPhoto? {
              let sticker = extractSticker(from: data)
              let colors = extractColors(from: data)
      
              guard let sticker = sticker, let colors = colors else { return nil }
      
              return ProcessedPhoto(sticker: sticker, colorScheme: colors)
          }
      
          private func extractColors(from data: Data) -> PhotoColorScheme? {
              // ...
          }
      
          private func extractSticker(from data: Data) -> Image? {
              // ...
          }
      }
    • 15:31 - Running the photo processing operations off the main thread

      func loadPhoto(_ item: SelectedPhoto) async {
          var data: Data? = try? await item.loadTransferable(type: Data.self)
      
          if let cachedData = getCachedData(for: item.id) { data = cachedData }
      
          guard let data else { return }
          processedPhotos[item.id] = await PhotoProcessor().process(data: data)
      
          cacheData(item.id, data)
      }
    • 20:55 - Running sticker and color extraction in parallel.

      nonisolated struct PhotoProcessor {
      
          @concurrent
          func process(data: Data) async -> ProcessedPhoto? {
              async let sticker = extractSticker(from: data)
              async let colors = extractColors(from: data)
      
              guard let sticker = await sticker, let colors = await colors else { return nil }
      
              return ProcessedPhoto(sticker: sticker, colorScheme: colors)
          }
      
          private func extractColors(from data: Data) -> PhotoColorScheme? {
              let colorExtractor = ColorExtractor()
              return colorExtractor.extractColors(from: data)
          }
      
          private func extractSticker(from data: Data) -> Image? {
              // ...
          }
      }
    • 24:20 - Applying the visual effect on each sticker in the carousel

      import SwiftUI
      import PhotosUI
      
      struct StickerCarousel: View {
          @State var viewModel: StickerViewModel
          @State private var sheetPresented: Bool = false
      
          var body: some View {
              ScrollView(.horizontal) {
                  LazyHStack(spacing: 16) {
                      ForEach(viewModel.selection) { selectedPhoto in
                          VStack {
                              if let processedPhoto = viewModel.processedPhotos[selectedPhoto.id] {
                                  GradientSticker(processedPhoto: processedPhoto)
                              } else if viewModel.invalidPhotos.contains(selectedPhoto.id) {
                                  InvalidStickerPlaceholder()
                              } else {
                                  StickerPlaceholder()
                                      .task {
                                          await viewModel.loadPhoto(selectedPhoto)
                                      }
                              }
                          }
                          .containerRelativeFrame(.horizontal)
                          .visualEffect { [selection = viewModel.selection] content, proxy in
                              let frame = proxy.frame(in: .scrollView(axis: .horizontal))
                              let distance = min(0, frame.minX)
                              let isLast = selectedPhoto.id == selection.last?.id
                              
                              return content
                                  .hueRotation(.degrees(frame.origin.x / 10))
                                  .scaleEffect(1 + distance / 700)
                                  .offset(x: isLast ? 0 : -distance / 1.25)
                                  .brightness(-distance / 400)
                                  .blur(radius: isLast ? 0 : -distance / 50)
                                  .opacity(isLast ? 1.0 : min(1.0, 1.0 - (-distance / 400)))
                          }
                      }
                  }
              }
              .configureCarousel(
                  viewModel,
                  sheetPresented: $sheetPresented
              )
              .sheet(isPresented: $sheetPresented) {
                  StickerGrid(viewModel: viewModel)
              }
          }
      }
    • 26:15 - Accessing a reference type from a concurrent task

      Task { @concurrent in
          await viewModel.loadPhoto(selectedPhoto)      
      }
    • 29:00 - Processing all photos at once with a task group

      func processAllPhotos() async {
          await withTaskGroup { group in
              for item in selection {
                  guard processedPhotos[item.id] == nil else { continue }
                  group.addTask {
                      let data = await self.getData(for: item)
                      let photo = await PhotoProcessor().process(data: data)
                      return photo.map { ProcessedPhotoResult(id: item.id, processedPhoto: $0) }
                  }
              }
      
              for await result in group {
                  if let result {
                      processedPhotos[result.id] = result.processedPhoto
                  }
              }
          }
      }
    • 30:00 - Kicking off photo processing and configuring the share link in a sticker grid view.

      import SwiftUI
      
      struct StickerGrid: View {
          let viewModel: StickerViewModel
          @State private var finishedLoading: Bool = false
      
          var body: some View {
              NavigationStack {
                  VStack {
                      if finishedLoading {
                          GridContent(viewModel: viewModel)
                      } else {
                          ProgressView()
                              .frame(maxWidth: .infinity, maxHeight: .infinity)
                              .padding()
                      }
                  }
                  .task {
                      await viewModel.processAllPhotos()
                      finishedLoading = true
                  }
                  .toolbar {
                      ToolbarItem(placement: .topBarTrailing) {
                          if finishedLoading {
                              ShareLink("Share", items: viewModel.selection.compactMap {
                                  viewModel.processedPhotos[$0.id]?.sticker
                              }) { sticker in
                                  SharePreview(
                                      "Sticker Preview",
                                      image: sticker,
                                      icon: Image(systemName: "photo")
                                  )
                              }
                          }
                      }
                  }
                  .configureStickerGrid()
              }
          }
      }

Developer Footer

  • Vidéos
  • WWDC25
  • Code-along : Améliorez une app avec la concurrence Swift
  • 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