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

Videos

Abrir menú Cerrar menú
  • Colecciones
  • Temas
  • Todos los videos
  • Información

Volver a WWDC25

  • Información
  • Transcripción
  • Código
  • Codificación conjunta: Mejora una app con la concurrencia en Swift

    Aprende a optimizar la experiencia del usuario de tu app con la concurrencia en Swift mientras actualizamos una app de muestra existente. Comenzaremos con una app de main actor y luego introduciremos gradualmente código asincrónico según sea necesario. Usaremos tareas para optimizar el código que se ejecuta en el main actor y descubriremos cómo paralelizar el código descargando trabajo en segundo plano. Exploraremos lo que proporciona la seguridad de carrera de datos y trabajaremos en la interpretación y solución de errores de seguridad de carrera de datos. Por último, mostraremos cómo aprovechar al máximo la concurrencia estructurada en el contexto de una app.

    Capítulos

    • 0:00 - Introducción
    • 2:11 - Configuración de concurrencia accesible
    • 2:51 - Arquitectura de app de muestra
    • 3:42 - Carga asincrónica de fotografías desde la biblioteca de fotografías
    • 9:03 - Extraer el sticker y los colores de la foto
    • 12:30 - Iniciar tareas en un hilo en segundo plano
    • 15:58 - Paralelización de tareas
    • 18:44 - Cómo evitar las carreras de datos con Swift 6
    • 27:56 - Controlar código asincrónico con concurrencia estructurada
    • 31:36 - Conclusión

    Recursos

    • Code-along: Elevating an app with Swift concurrency
    • Swift Migration Guide
      • Video HD
      • Video SD

    Videos relacionados

    WWDC25

    • Adopción de la concurrencia en Swift
    • Explora la concurrencia en SwiftUI

    WWDC23

    • Analyze hangs with Instruments
    • Beyond the basics of structured concurrency
  • Buscar este video…

    ¡Hola! Soy Sima y trabajo en Swift y SwiftUI. En este video, aprenderás cómo mejorar tu app con la concurrencia en Swift. Como desarrolladores de apps, encontramos más del código en el hilo principal.

    El código de un solo subproceso es fácil de entender y mantener, pero al mismo tiempo, una app moderna a menudo debe hacer tareas que llevan mucho tiempo, como una solicitud de red o un cálculo complejo. En esos casos, es mejor sacar el trabajo del hilo principal para que la app siga respondiendo bien. Swift te brinda todas las herramientas que necesitas para escribir código concurrente con confianza. En esta sesión, te mostraré cómo hacerlo creando una app contigo. Comenzaremos con una app de un solo subproceso y, luego, iremos agregando código asincrónico cuando sea necesario. Luego, mejoraremos el rendimiento de la app al sacar algunas de las tareas más pesadas y ejecutarlas en paralelo. Después, analizaremos algunos escenarios comunes de seguridad de carrera de datos con los que te puedes encontrar y veremos cómo abordarlos. Por último, abordaré la concurrencia estructurada y te mostraré cómo utilizar herramientas como TaskGroup para tener un mayor control sobre tu código concurrente. ¡No puedo esperar para empezar! Me encanta llevar un diario y decorar mis anotaciones con stickers, así que te voy a enseñar a crear una app para armar paquetes de stickers a partir de cualquier conjunto de fotos. Nuestra app tendrá dos vistas principales. La primera mostrará un carrusel con todos los stickers con un degradado que refleja los colores de la foto original, y la segunda mostrará una vista previa en cuadrícula de todo el paquete de stickers, listo para exportar. ¡Siéntete libre de descargar la app de muestra para seguir el tutorial! Cuando creé el proyecto, Xcode habilitó algunas funciones que facilitan la introducción de la concurrencia, como el modo actor principal por defecto y algunas funciones futuras. Estas funciones están habilitadas por defecto en los nuevos proyectos de apps en Xcode 26.

    En la configuración de concurrencia accesible, el modo de lenguaje Swift 6 te dará seguridad contra conflictos de datos sin introducir concurrencia hasta que estés listo. Si deseas habilitar estas funciones en proyectos existentes, puedes aprender cómo hacerlo en la guía de migración de Swift.

    En el código, la app tendrá dos vistas principales: StickerCarousel y StickerGrid. Estas vistas usarán los stickers que la estructura PhotoProcessor se encarga de extraer.

    PhotoProcessor obtiene la imagen sin procesar de la biblioteca de fotos antes de generar el sticker.

    La vista StickerGrid tiene un ShareLink que puedes usar para compartir los stickers.

    El tipo PhotoProcessor realiza dos operaciones complejas: la extracción de stickers y el cálculo de los colores dominantes. Veamos cómo las funciones de concurrencia de Swift pueden ayudarnos a optimizar la experiencia del usuario, sin impedir que el dispositivo ejecute estas tareas tan complejas. Comenzaré con la vista StickerCarousel. Esta vista muestra los stickers en una vista de desplazamiento horizontal. Dentro de ScrollView, hay un ForEach que itera sobre la matriz de fotos seleccionadas de la biblioteca de fotos almacenada en el modelo de vista. Revisa el diccionario processedPhotos en el viewModel para obtener la foto procesada que corresponde a la selección en la biblioteca de fotos. Actualmente, no hay ninguna foto procesada, ya que no he escrito ningún código para obtener una imagen del selector de fotos. Si ejecuto la app ahora, todo lo que veremos en la vista de desplazamiento es la vista StickerPlaceholder. Iré a StickerViewModel con la tecla Command y clic. StickerViewModel almacena una matriz de fotos seleccionadas actualmente de la biblioteca de fotos, representadas como un tipo SelectedPhoto. Abriré Ayuda rápida con la tecla Opción y tocaré para obtener información de este tipo.

    SelectedPhoto es un tipo identificable que almacena un PhotosPickerItem de la estructura PhotosUI y su ID asociado. El modelo también tiene el diccionario, processedPhotos, que asigna el ID de la foto seleccionada a la imagen SwiftUI que representa. Yo ya comencé a trabajar en la función loadPhoto que toma la foto seleccionada, pero actualmente no carga ningún dato del elemento selector de fotografías que almacena. PhotosPickerItem se ajusta al protocolo Transferable del SDK, lo que me permite cargar la representación que solicito con la función asincrónica loadTransferable. Solicitaré la representación de los datos.

    Ahora hay un error del compilador.

    Se debe a que la ejecución de `loadTransferable` es asincrónica y la función `loadPhoto` donde la ejecuto no está configurada para manejar ejecuciones asincrónicas, por lo que Swift me ayuda sugiriendo marcar `loadPhoto` con la palabra clave async. Voy a aplicar esta sugerencia.

    Ya la función es capaz de manejar código asincrónico, pero aún queda un error más. Si bien `loadPhoto` puede manejar ejecuciones asincrónicas, necesitamos indicarle qué esperar. Para hacerlo, necesito marcar la ejecución de `loadTransferable` con la palabra clave `await`. Aplicaré la solución sugerida.

    Vamos a ejecutar esta función en la vista StickerCarousel. Si presiono Command, Shift, O, puedo usar Open Quickly de Xcode para volver a StickerCarousel.

    Me gustaría ejecutar loadPhoto cuando aparezca la vista StickerPlaceholder. Como esta función es asincrónica, usaré el modificador de tarea SwiftUI para iniciar el procesamiento de fotografías cuando aparezca esta vista.

    Veamos cómo funciona en mi dispositivo.

    Genial, ya funciona. Intentemos seleccionar algunas fotos para probarla.

    ¡Excelente! Parece que las imágenes se están cargando desde mi biblioteca de fotos. La tarea me permite mantener la interfaz de usuario de la app receptiva mientras se carga la imagen desde los datos. Y como uso LazyHStack para mostrar las imágenes, solo inicio las tareas de carga de fotos para las vistas que deben mostrarse en pantalla, por lo que la app no hace más trabajo del necesario. Hablemos de por qué async/await mejora la capacidad de respuesta de nuestra app.

    Agregamos la palabra clave `await` al ejecutar el método `loadTransferable` y anotamos la función `loadPhoto` con `async`. `await` marca un posible punto de suspensión. Significa que, al principio, la función loadPhoto se inicia en el hilo principal y, cuando ejecuta loadTransferable en el await, se suspende mientras espera a que loadTransferable termine. Mientras loadPhoto esté suspendida, la estructura Transferable ejecutará loadTransferable en el hilo en segundo plano. Cuando se complete loadTransferable, loadPhoto reanudará su ejecución en el hilo principal y actualizará la imagen. El hilo principal es libre de responder a eventos de UI y ejecutar otras tareas mientras loadPhoto está suspendida. La palabra clave await indica un punto en tu código donde se puede hacer otro trabajo mientras tu función está suspendida. Y eso es todo, hemos terminado de cargar las imágenes de la biblioteca de fotos. En el camino, aprendimos qué significa el código asincrónico, cómo escribirlo y pensar en él. Ahora, añadamos código a nuestra app para extraer el sticker de la foto y sus colores principales, que podremos usar para el degradado de fondo cuando se muestre en una vista de carrusel.

    Voy a presionar Command y clic para regresar a loadPhoto donde puedo aplicar estos efectos.

    El proyecto ya incluye un PhotoProcessor, que toma los datos, extrae los colores y el sticker y devuelve la foto procesada. Usaré PhotoProcessor en vez de la imagen básica de datos.

    PhotoProcessor genera una foto procesada, por lo que actualizaré el tipo del diccionario.

    ProcessedPhoto nos proporcionará el sticker extraído de la foto y la matriz de colores para componer el degradado.

    Ya incluí una vista GradientSticker en el proyecto que toma processedPhoto. Voy a usar Open Quickly para ir a ella.

    Esta vista muestra un sticker almacenado en una foto procesada sobre un degradado lineal en un ZStack.

    Voy a agregar este GradientSticker en el carrusel.

    Actualmente, en StickerCarousel solo estamos cambiando el tamaño de la foto, pero ahora que tenemos una foto procesada, podemos usar el GradientSticker aquí.

    Vamos a crear y ejecutar la app para ver nuestros stickers.

    ¡Funciona!

    Oh, no. Mientras se extraen los stickers, no es tan fácil desplazarse por el carrusel.

    Me parece que el procesamiento de imágenes es muy complejo. Hice un perfil de la app usando Instruments para confirmarlo. El seguimiento muestra que nuestra app tiene bloqueos graves.

    Si amplio y miro el seguimiento de la pila más pesado, puedo ver que el procesador de fotos bloquea el hilo principal con las tareas de procesamiento complejas por más de 10 segundos. Si quieres aprender más sobre cómo analizar bloqueos en tu app, consulta la sesión “Analyze hangs with Instruments”. Ahora, hablemos más sobre el trabajo que la app está realizando en el hilo principal.

    La implementación de `loadTransferable` se encargó de trasladar el trabajo al segundo plano para evitar que la carga se realizara en el hilo principal.

    Ahora que añadimos el código de procesamiento de imágenes, que se ejecuta en el hilo principal y tarda en completarse, el hilo principal no puede recibir ninguna actualización de la IU, como responder a los gestos de desplazamiento, lo que provoca una mala experiencia de usuario en la app.

    Anteriormente, adoptamos una API asíncrona del SDK, que liberaba trabajo por nosotros. Ahora debemos ejecutar nuestro propio código en paralelo para solucionar el problema. Podemos mover algunas de las transformaciones de imagen al segundo plano. Transformar la imagen implica estas tres operaciones. Obtener la imagen sin procesar y actualizarla implica interactuar con la IU, así que no podemos mover este trabajo a segundo plano, pero sí podemos sacar el procesamiento de imágenes. Esto garantizará que el hilo principal pueda responder a otros eventos mientras se lleva a cabo el complejo trabajo de procesamiento de imágenes. Veamos la estructura PhotoProcessor para entender cómo podemos hacer esto.

    Como mi app está en modo actor principal por defecto, PhotoProcessor está vinculado a @MainActor, lo que significa que todos sus métodos deben ejecutarse en actor principal. El método `process` ejecuta los métodos extractSticker y extractColors, así que debo marcar todos los métodos de este tipo como capaces de ejecutarse fuera del actor principal. Para ello, puedo marcar todo el tipo de PhotoProcessor con no aislado. Esta es una nueva función introducida en Swift 6.1. Cuando el tipo se marca como no aislado, todas sus propiedades y métodos quedan automáticamente no aislados.

    Ahora que PhotoProcessor no está vinculado a MainActor, podemos aplicar el nuevo atributo `@concurrent` a la función de proceso y marcarlo con `async`. Esto le indicará a Swift que siempre cambie a un hilo en segundo plano cuando ejecute este método. Usaré Open Quickly para regresar a PhotoProcessor.

    Primero, voy a aplicar nonisolated al tipo para desacoplar PhotoProcessor del actor principal y permitir que sus métodos se ejecuten desde código concurrente.

    Ahora que PhotoProcessor no está aislado, para asegurarme de que el método process se ejecute desde el subproceso en segundo plano, aplicaré @concurrent y async.

    Ahora, regresaré a StickerViewModel con Open Quickly.

    Aquí, en el método loadPhoto, necesito salir del hilo principal ejecutando al método process con la palabra clave `await`, que Swift ya sugiere. Voy a aplicar esta sugerencia.

    Compilemos y ejecutemos la app para ver si quitar esta parte del actor principal solucionó los fallos.

    Parece que no hay más problemas en el desplazamiento.

    Pero aunque puedo interactuar con la IU, la imagen tarda un tiempo en aparecer mientras me desplazo. Mantener la capacidad de respuesta de una app no es el único factor para mejorar la experiencia de usuario. Si sacamos el trabajo del hilo principal, pero tarda mucho tiempo en mostrar los resultados al usuario, la experiencia de uso de la app puede seguir siendo frustrante.

    Movimos la operación de procesamiento de imágenes a un hilo en segundo plano, pero aún lleva mucho tiempo completarla. Veamos cómo podemos optimizar esta operación con concurrencia para que se complete más rápido. El procesamiento de la imagen requiere la extracción de los stickers y los colores dominantes, pero estas operaciones son independientes entre sí. De esta manera podemos ejecutar estas tareas en paralelo con async let. Ahora, el grupo de subprocesos concurrentes, que gestiona todos los subprocesos en segundo plano, programará estas dos tareas para que se inicien en dos subprocesos en segundo plano diferentes a la vez. Esto me permite aprovechar múltiples núcleos en mi teléfono.

    Presionaré Command y clic en el método process para adoptar async let.

    Al mantener pulsadas las teclas Control + shift y la flecha hacia abajo, puedo utilizar el cursor multilínea para añadir async delante de las variables sticker y colors.

    Ahora que ejecutamos estas dos en paralelo, debemos esperar sus resultados para reanudar nuestra función de proceso. Solucionemos todos estos problemas con el menú Editor.

    Pero aún queda un error. Esta vez se trata de una carrera de datos. Tomémonos un tiempo para entender este error.

    Este error significa que mi tipo de PhotoProcessor no es seguro para compartir entre tareas simultáneas. Para entender por qué, veamos sus propiedades almacenadas. La única propiedad que almacena PhotoProcessor es una instancia de ColorExtractor, necesaria para extraer los colores de la foto. La clase ColorExtractor calcula los colores dominantes que aparecen en la imagen. Este cálculo opera sobre datos de imagen mutables de bajo nivel, incluidos los búferes de píxeles, por lo que no es seguro acceder simultáneamente al tipo de extractor de color.

    En este momento, todas las operaciones de extracción de color comparten la misma instancia de ColorExtractor. Esto da lugar a un acceso simultáneo a la misma memoria.

    Esto se denomina “carrera de datos”, lo que puede provocar errores de ejecución, como bloqueos y comportamientos impredecibles. El modo de lenguaje Swift 6 identificará estos errores en tiempo de compilación, lo que elimina este conjunto de errores cuando se escribe código que se ejecuta en paralelo. Esto convierte lo que habría sido un complejo error de tiempo de ejecución en un error de compilación que se puede solucionar de inmediato. Si haces clic en el botón “ayuda” en el mensaje de error, puedes obtener más información sobre este error en el sitio web de Swift. Hay varias opciones que puedes considerar al intentar resolver una carrera de datos. La elección de uno depende de cómo tu código utiliza los datos compartidos. Primero, pregúntate: ¿Es necesario compartir este estado mutable entre códigos concurrentes? En muchos casos, basta con evitar compartirlo. Sin embargo, hay casos en los que el código debe compartir el estado. Si ese es el caso, considera extraer lo que necesitas compartir a un tipo de valor que sea seguro enviar. Solo si ninguna de estas soluciones es aplicable a tu situación, considera aislar este estado en un actor como MainActor. Veamos si la primera solución funciona en nuestro caso. Aunque podríamos refactorizar este tipo para que funcione de manera diferente y pueda gestionar múltiples operaciones simultáneas, también podemos mover el extractor de color a una variable local en la función extractColors, de modo que cada foto procesada tenga su propia instancia del extractor de color. Este es el cambio de código correcto, ya que el extractor de color está diseñado para funcionar con una sola foto a la vez. Así que queremos una instancia separada para cada tarea de extracción de color. Con este cambio, nada fuera de la función extractColors puede acceder al extractor de color, lo que evita la carrera de datos.

    Para hacer este cambio, movamos la propiedad del extractor de color a la función extractColors.

    ¡Excelente! Con la ayuda del compilador, pudimos detectar y eliminar una carrera de datos en nuestra app. Ahora, vamos a ejecutarlo.

    Puedo sentir que la app funciona más rápido.

    Si recopilo un seguimiento del generador de perfiles en Instruments y lo abro, ya no veo los bloqueos. Vamos a repasar un poco las optimizaciones que hicimos con la concurrencia en Swift. Al adoptar el atributo `@concurrent`, logramos trasladar nuestro código de procesamiento de imágenes fuera del hilo principal. También paralelizamos sus operaciones, la extracción de stickers y colores entre sí con `async let`, lo que hizo que nuestra app funcionara mucho mejor. Las optimizaciones que hagas con la concurrencia en Swift siempre deben basarse en datos de herramientas de análisis, como el instrumento Time Profiler. Si puedes hacer que tu código sea más eficiente sin introducir concurrencia, siempre debes hacerlo primero. Ahora la app anda más rápido. Demos un respiro al procesamiento de imágenes y añadamos algo divertido.

    Añadamos un efecto visual a nuestros stickers procesados que haga que al pasar el sticker se desvanezca y se difumine. Vamos a Xcode para escribir eso.

    Regresaré a StickerCarousel con el navegador de proyectos Xcode.

    Ahora, voy a aplicar el efecto visual en cada imagen en la vista de desplazamiento con el modificador visualEffect.

    Aquí estoy aplicando algunos efectos a la vista. Quiero cambiar el desplazamiento, el desenfoque y la opacidad solo para el último sticker en la vista de desplazamiento, así que necesito acceder a la propiedad de selección del viewModel para comprobar si el efecto visual se aplica al último sticker.

    Parece que hay un error de compilación porque estoy intentando acceder al estado de vista protegido del actor principal desde el cierre visualEffect. Dado que el cálculo de un efecto visual es una operación compleja, SwiftUI lo traslada fuera del hilo principal para maximizar el rendimiento de mi app.

    Si te apetece la aventura y quieres aprender más, echa un vistazo a nuestra sesión Explore concurrency in SwiftUI. Esto es lo que me dice este error: este cierre se evaluará más adelante en segundo plano. Confirmemos esto mirando la definición de `visualEffect`, presionemos Command y clic.

    En la definición, este cierre es @Sendable, lo que es una indicación de SwiftUI de que este cierre se evaluará en segundo plano.

    En este caso, SwiftUI vuelve a ejecutar el efecto visual cada vez que cambia la selección, así que puedo hacer una copia con la lista de captura del cierre.

    Ahora, cuando SwiftUI ejecute este cierre, operará sobre una copia del valor de selección, lo que hará que esta operación no genere una carrera de datos.

    Veamos nuestro efecto visual.

    Se ve genial y puedo ver cómo la imagen anterior se difumina y se desvanece a medida que me desplazo.

    En ambos escenarios de carrera de datos que hemos encontrado, la solución fue no compartir datos que puedan mutarse desde el código concurrente. La diferencia clave fue que en el primer ejemplo introduje yo misma una carrera de datos ejecutando parte del código en paralelo. Sin embargo, en el segundo ejemplo, utilicé una API SwiftUI que mueve el trabajo al hilo en segundo plano por mi.

    Si debes compartir un estado mutable, existen otras formas de protegerlo. Los tipos de valores enviables impiden que el tipo pueda ser compartido por código concurrente. Por ejemplo, los métodos extractSticker y extractColors se ejecutan en paralelo y ambos toman los datos de la misma imagen. Pero en este caso no existe una condición de carrera de datos porque los datos son un tipo de valor que se puede enviar. Los datos también implementan copia en escritura, por lo que solo se copian si sufren una mutación. Si no puedes usar un tipo de valor, puedes considerar aislar tu estado del actor principal. Afortunadamente, el actor principal por defecto ya lo hace por ti. Por ejemplo, nuestro modelo es una clase y podemos acceder a él desde una tarea concurrente. Debido a que el modelo está marcado implícitamente con MainActor, es seguro hacer referencia a él desde el código concurrente. El código tendrá que cambiar al actor principal para acceder al estado. En este caso, la clase está protegida por el actor principal, pero lo mismo se aplica a otros actores que puedas tener en tu código. Nuestra app se ve genial hasta ahora, pero aún no se siente completa.

    Para poder exportar los stickers, agreguemos una vista de cuadrícula de stickers que inicia una tarea de procesamiento de fotos para cada foto que aún no se haya procesado y muestra todos los stickers a la vez. También tendrá un botón para compartir que permitirá exportar estos stickers. Volvamos al código.

    Primero, presionaré Command y clic para navegar hasta StickerViewModel.

    Voy a agregar otro método a nuestro modelo, `processAllPhotos()`.

    Aquí quiero iterar sobre todas las fotos procesadas guardadas hasta ahora en mi modelo, y si aún hay fotos sin procesar, quiero iniciar varias tareas paralelas para comenzar a procesarlas todas a la vez.

    Ya usamos async let antes, pero solo funcionó porque sabíamos que solo había dos tareas que hacer: extraer el sticker y el color. Ahora, debemos crear una nueva tarea para todas las fotos sin procesar de la matriz, y puede haber cualquier cantidad de estas tareas de procesamiento.

    Las API como TaskGroup te permiten tener más control sobre el trabajo asincrónico que tu app debe realizar.

    TaskGroup ofrece un control detallado sobre las tareas secundarias y sus resultados. Permite iniciar cualquier número de areas secundarias que pueden ejecutarse en paralelo.

    Cada tarea secundaria puede tomar una cantidad arbitraria de tiempo para completarse, por lo que podrían realizarse en un orden diferente. En nuestro caso, las fotos procesadas se guardarán en un diccionario, por lo que el orden no importa.

    TaskGroup se ajusta a AsyncSequence, así que podemos iterar sobre los resultados a medida que se realizan para almacenarlos en el diccionario. Y por último podemos esperar a que todo el grupo termine las tareas secundarias. Volvamos al código para adoptar un grupo de tareas. Para adoptar el grupo de trabajo, comenzaré por declararlo.

    Aquí, dentro del cierre, tengo una referencia al grupo al que puedo agregar tareas de procesamiento de imágenes. Voy a iterar sobre la selección guardada en el modelo.

    Si esta foto ha sido procesada, entonces no necesito crear una tarea para ella.

    Comenzaré una nueva tarea de carga de datos y procesamiento de la foto.

    Como el grupo es una secuencia asincrónica, puedo iterar sobre él para guardar la foto procesada en el diccionario processPhotos una vez que esté lista.

    ¡Eso es todo! Estamos listos para mostrar nuestros stickers en StickerGrid.

    Usaré Open Quickly para ir a StickerGrid.

    Aquí tengo una propiedad de estado finishedLoading que indica si todas las fotos han terminado de procesarse.

    Si las fotos aún no se han procesado, se mostrará una vista del progreso. Voy a ejecutar el método processAllPhotos() que acabamos de implementar.

    Una vez procesadas todas las fotos, podemos establecer la variable de estado. ¡Y por último, agregaré el enlace para compartir en la barra de herramientas para compartir los stickers.

    Estoy colocando stickers en los elementos del enlace compartido para cada foto seleccionada.

    Ejecutemos la app.

    Tocaré el botón StickerGrid. Gracias a TaskGroup, la cuadrícula de vista previa comienza a procesar todas las fotos juntas. Y cuando estén listos, podré ver instantáneamente todos los stickers. Por último, con el botón Compartir en la barra de herramientas, puedo exportar todos los stickers como archivos que puedo guardar.

    En nuestra app, los stickers se recopilarán en el orden en que se hayan procesado, pero también puedes realizar un seguimiento del pedido, y TaskGroup tiene muchas más funciones. Para obtener más información, consulta la sesión “Beyond the basics of structured concurrency”.

    ¡Felicitaciones! La app está lista y ahora puedo guardar mis stickers. Agregamos nuevas funciones a una app, descubrimos cuándo tuvieron un impacto en la IU y usamos la concurrencia tanto como fue necesario para mejorar la capacidad de respuesta y el rendimiento. También aprendimos sobre concurrencia estructurada y cómo prevenir carreras de datos.

    Si no seguiste el proceso, aún puedes descargar la versión final de la app y crear stickers con tus propias fotos. Para familiarizarte con las nuevas funciones y técnicas de concurrencia en Swift mencionadas en esta charla, intenta optimizar o ajustar aún más la app. Por último, ve si puedes llevar estas técnicas a tu app. ¡Recuerda crear un perfil! Para profundizar en la comprensión de los conceptos del modelo de concurrencia en Swift, consulta nuestra sesión “Embracing Swift concurrency”. Para migrar tu proyecto existente para adoptar nuevas funciones de concurrencia accesibles, consulta la "Swift Migration Guide". ¡Mi parte favorita fue que conseguí algunos stickers para mi cuaderno! ¡Gracias por unirte!

    • 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

  • Videos
  • WWDC25
  • Codificación conjunta: Mejora una app con la concurrencia en 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