Concurrencia estricta en Swift 6: guía de migración para aplicaciones existentes
La concurrencia estricta de Swift 6 convierte las condiciones de carrera en errores de compilación. Guía módulo por módulo para migrar una app existente sin reescribirla.
Migra una aplicación existente a la concurrencia estricta de Swift 6 un target a la vez, no en una sola versión. La secuencia de Apple funciona en bases de código de cualquier tamaño: habilita Complete Concurrency Checking en modo Swift 5, resuelve las advertencias y luego pasa ese target al modo de lenguaje Swift 6. Cada módulo queda fijado de forma independiente. Esta guía recorre esa secuencia, los errores más comunes y dónde Swift 6.2 cambia el cálculo para los equipos que empiezan la migración hoy.
Antes de empezar
La verificación de concurrencia estricta existe para detectar condiciones de carrera en tiempo de compilación, en lugar de en tiempo de ejecución o, peor aún, en el registro de errores de un cliente. Aplica dos ideas: aislamiento de actores, que define qué código puede tocar qué estado, y la conformidad Sendable, que define qué valores son seguros para cruzar ese límite. Las aplicaciones construidas antes de async y await suelen acumular exactamente los patrones que marca la verificación estricta: variables globales mutables, protocolos delegate sin aislamiento definido y frameworks de terceros anteriores al modelo de concurrencia.
Fija expectativas antes de empezar. La sesión de migración de WWDC24 de Apple es explícita en que esto debe ser un paso independiente, separado del trabajo de nuevas funciones o de una refactorización más amplia. Si mezclas ambas cosas, cada regresión se vuelve difícil de atribuir: ¿fue la nueva función o el cambio de concurrencia debajo de ella? La misma disciplina aplica a cualquier base de código SwiftUI que tu equipo publique, el mismo trabajo de fondo que cubrimos en shipping SwiftUI apps with confidence.
También hay una bifurcación que conviene conocer desde el principio. Swift 6.2, lanzado en 2025, introdujo una rampa de entrada más sencilla llamada Approachable Concurrency, cubierta en la sección paso a paso más abajo. Los equipos que empiezan una migración ahora tienen una elección real entre el modelo original y el nuevo enfoque de aislamiento por defecto.
Antes de tocar código, haz un inventario rápido. Enumera cada target y paquete del que depende tu aplicación. Anota cuáles son propios y cuáles de terceros, y marca las dependencias de terceros que aún no han publicado una versión compatible con Swift 6. Una dependencia atascada en un modelo de concurrencia antiguo puede impedir que un target llegue al modo Swift 6 completo incluso después de que tu propio código esté limpio, así que conviene saberlo desde el principio y no descubrirlo a mitad de la migración.
Paso a paso
1. Habilita Complete Concurrency Checking, un target a la vez
Activa el ajuste de compilación para un solo módulo mientras el resto del proyecto permanece en modo Swift 5. Esto no rompe la compilación. En cambio, expone cada error que el modo Swift 6 terminaría exigiendo, pero como advertencia, para que puedas ver el alcance completo del trabajo antes de comprometerte con él. La recomendación de Apple es tratar al compilador como un revisor: señala problemas de aislamiento reales y específicos en lugar de obligarte a adivinar dónde está el riesgo.
2. Resuelve las advertencias de ese target
Cuatro categorías explican la mayoría de las advertencias que ve una aplicación típica.
El estado mutable global o estático genera un error de "no seguro para concurrencia", porque más de un hilo podría leerlo o escribirlo a la vez. La solución depende del valor. Conviértelo en un let inmutable cuando nada necesita cambiarlo después de crearlo. Aíslalo a un actor como @MainActor cuando debe seguir siendo mutable pero solo un actor lo toca. O, como último recurso, márcalo nonisolated(unsafe) cuando ya existe sincronización externa y puedes demostrarlo.
Los callbacks de delegate son la segunda fuente común, ya que un protocolo simple no garantiza en qué actor se ejecutan sus métodos. Si el protocolo viene de un framework que no controlas, marca tu conformidad como @preconcurrency para adoptarlo de forma incremental. ¿Necesitas un punto de entrada síncrono que salte de inmediato al actor correcto? Marca el método nonisolated y envuelve su cuerpo en MainActor.assumeIsolated. Si tú controlas el protocolo, la solución más limpia es marcarlo @MainActor directamente.
Los valores que cruzan un límite de actor necesitan conformidad Sendable explícita. Swift no infiere Sendable para tipos públicos automáticamente, ya que declarar un tipo como Sendable es una garantía de API pública, no un detalle de implementación. Los tipos por valor suelen ser una solución rápida: los structs y enums se vuelven Sendable en cuanto cada propiedad almacenada lo es. Los tipos por referencia son el caso más difícil. Una clase normalmente necesita convertirse en actor, o implementar seguridad de hilos y Sendable manualmente.
Un cuarto patrón aparece cuando un tipo de parámetro no puede, o no puede fácilmente, conformar a Sendable. En lugar de forzar ese tipo a cruzar el límite, acepta un closure @Sendable que construya el valor. El closure cruza de forma segura y construye el tipo no Sendable en el lado correcto del límite, evitando el requisito de conformidad sin debilitar la garantía de seguridad.
3. Pasa el target al modo de lenguaje Swift 6 y repite
Una vez que las advertencias de un target están resueltas, habilita el modo de lenguaje Swift 6 específicamente para ese target. Esto convierte los mismos diagnósticos en errores de compilación, fijando el trabajo y evitando que un cambio futuro reintroduzca silenciosamente una condición de carrera. Pasa al siguiente target y repite los mismos tres pasos. Un paso de refactorización de toda la aplicación, si lo quieres, llega después de migrar cada target, no antes.
4. Considera la vía de Approachable Concurrency de Swift 6.2
Para los equipos que empiezan la migración hoy, Swift 6.2 ofrece un comportamiento por defecto sustancialmente distinto. En lugar de tratar cada declaración como aislada salvo que se demuestre lo contrario, un target puede optar por el aislamiento MainActor por defecto, de modo que los view models y las vistas SwiftUI ya no necesitan anotación manual una por una. Esto encaja bien en targets de aplicación y paquetes centrados en interfaz. Las librerías de utilidades y el código de backend deberían, en general, mantener el comportamiento original sin aislamiento por defecto.
Habilita las cinco funciones de Approachable Concurrency de Swift 6.2 de una en una en lugar de todas a la vez, y deja NonisolatedNonsendingByDefault para el final, ya que es la única que cambia el comportamiento en tiempo de ejecución y no solo los diagnósticos. Antes de activarla, audita tus funciones asíncronas: marca el trabajo realmente intensivo en CPU, como análisis de datos grandes o filtrado de imágenes, con @concurrent para que siga ejecutándose fuera del actor principal en lugar de heredar silenciosamente el contexto de quien la llama.
Errores comunes
Una propiedad mutable en un tipo que has marcado Sendable es el disparador más común de un error de migración. La solución depende del tipo: una clase generalmente necesita convertirse en actor u obtener seguridad de hilos real, mientras que un struct solo necesita que se le añada Sendable una vez que sus propiedades almacenadas califican.
Convertir una clase en actor tiene un costo que conviene planificar, no descubrir a mitad de la migración. Un actor es implícitamente final, y cada método que toca su estado se vuelve asíncrono, así que cada punto de llamada ahora necesita un await. Esa es una decisión de diseño deliberada del lenguaje, y se propaga hacia afuera por cada llamador de ese tipo.
No toda advertencia restante es culpa de tu aplicación. Varios frameworks de Apple aún no han adoptado por completo la concurrencia estricta: SwiftData fuera de los tipos ModelActor, Combine, XPC y el framework Virtualization son ejemplos comunes. Trata una advertencia persistente en uno de estos como un vacío del framework para seguir de cerca, no como un error en tu propio código que perseguir indefinidamente, y envuelve el callback heredado en un CheckedContinuation cuando necesites conectarlo con código asíncrono hoy mismo.
Fijar una sola fecha límite para toda la aplicación también es un error común. Un calendario por target, seguido target por target en tu tablero de proyecto habitual, resiste mucho mejor la realidad que una única fecha de "listo para" para toda la base de código, porque los targets varían enormemente en cuánto estado heredado cargan.
Esta disciplina de módulo por módulo, verificar un target a la vez antes de pasar al siguiente, es la misma puerta de cumplimiento que Kallos Labs aplica internamente antes de cada envío a App Store, parte de nuestra práctica de desarrollo iOS.
Preguntas frecuentes
¿Tengo que migrar toda mi aplicación a Swift 6 de una sola vez?
No. La verificación de concurrencia estricta de Swift 6 se puede habilitar por target mientras el resto de la aplicación permanece en modo Swift 5, de modo que una aplicación grande se migra módulo por módulo en lugar de en una sola versión.
¿Cuál es la diferencia entre Complete Concurrency Checking y el modo de lenguaje Swift 6?
Complete Concurrency Checking activa diagnósticos estrictos de condiciones de carrera como advertencias dentro del modo Swift 5, permitiendo que un target compile y se ejecute mientras expone cada problema que fallaría bajo Swift 6. El modo de lenguaje Swift 6 es el segundo paso: convierte esos mismos diagnósticos en errores de compilación, fijando el target.
¿Por qué el compilador dice que una variable global no es segura para concurrencia?
El estado mutable global o estático puede leerse o escribirse desde más de un hilo a la vez, algo que la concurrencia estricta trata como una posible condición de carrera. Convertir el valor en un let inmutable, aislarlo a un actor o marcarlo nonisolated(unsafe) cuando ya está protegido por sincronización existente resuelve el error.
¿Convertir una clase en actor rompe su API pública?
Por lo general, sí, de forma visible. Un actor es implícitamente final, y cada método que toca su estado se vuelve asíncrono, así que cada punto de llamada necesita un await. Es un costo de diseño deliberado, no un error, y conviene planificarlo antes de empezar la conversión.