← Todos los articulos

Concurrencia en Swift 6.2 en la práctica: por defecto a MainActor, sal solo cuando lo decidas

Swift 6.0 convirtió las carreras de datos en un error de compilación y, durante un año, hizo que todos lo pagaran. El verificador de concurrencia estricta transformó el código de UI corriente en un muro de violaciones de Sendable y errores del tipo “main actor-isolated property can not be referenced from a nonisolated context”. El diagnóstico era correcto (ese código sí podía sufrir carreras), pero el volumen sepultó la señal, y muchos equipos optaron por quedarse en el modo Swift 5 o por rociar @MainActor hasta que los errores se callaran.

Swift 6.2 cambia el comportamiento por defecto en lugar de las reglas. La garantía de seguridad frente a carreras de datos es la misma; lo que cambió es el punto de partida del compilador. Adopta los nuevos valores por defecto y la mayor parte del muro desaparece, porque ahora el compilador asume lo que tu app ya cumplía: casi todo tu código corre en el main actor, y lo abandonas a propósito, en lugares con nombre. Este es el modelo que envío en las apps de la familia 9411, y es una pieza de un arco de evolución más amplio que el resumen de novedades de Swift para 2026 recorre a través de las propuestas 6.3 y 6.4 que vinieron después. Así funciona, y estos son los seis errores que aún muerden una vez que lo activas.

Resumen

  • SE-0466 te permite dejar un módulo entero por defecto en @MainActor. Configura SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor y deja de anotar cada vista, modelo y view-model2.
  • SE-0461 hace que las funciones async nonisolated corran en el actor de quien llama por defecto (nonisolated(nonsending)), de modo que llamar código async ya no fuerza un salto de actor ni los errores de Sendable que cruzaban la frontera que venían con él3.
  • @concurrent es la vía de escape. Marca una función como @concurrent para empujar trabajo pesado de CPU (decodificación, procesamiento de imágenes) a un hilo en segundo plano, de forma deliberada y visible4.
  • Xcode 26 activa ambos valores por defecto en los proyectos nuevos. El interruptor paraguas es SWIFT_APPROACHABLE_CONCURRENCY = YES5.
  • El modelo invierte la carga: en lugar de demostrar que cada línea es segura para correr fuera del main, mantienes todo en el main y demuestras los pocos lugares donde lo abandonas.
  • Seis errores concretos sobreviven al cambio. Los seis tienen arreglos de una línea una vez que reconoces el patrón.

La inversión, y por qué importa

Modelo antiguo (Swift 6.0): el código es nonisolated hasta que lo aíslas. Cada tipo que tocara estado de UI necesitaba @MainActor, cada llamada async que cruzara una frontera de aislamiento necesitaba conformidad con Sendable, y el compilador marcaba cada hueco. En una app donde el 95 por ciento del código ya corre en el hilo principal, gastabas tu tiempo anotando ese 95 por ciento para describir un hecho que nunca estuvo en duda.

Modelo nuevo (Swift 6.2): el código está en el main actor hasta que lo abandonas. SE-0466 te permite declarar el aislamiento en el main actor como valor por defecto del módulo, de modo que una vista, su modelo y sus helpers son todos @MainActor sin una sola anotación2. SE-0461 elimina entonces el segundo impuesto: una función async nonisolated ahora corre en el actor que la haya llamado en vez de saltar al ejecutor global, así que esperarla con await no te arrastra a través de una frontera de aislamiento ni exige Sendable sobre todo lo que esté en alcance3.

El modelo mental es el que coincide con cómo se comportan realmente las apps de UI. El hilo principal por defecto no es un compromiso; es la verdad de una app cuyo estado son sus vistas. La concurrencia pasa a ser la excepción a la que recurres, con nombre y contenida, en lugar de la condición ambiental contra la que te defiendes en cada línea. El trabajo del compilador pasa de “demuestra que esto es seguro para correr de forma concurrente” a “dijiste que esto corre de forma concurrente, así que demuestra que es seguro”, y la segunda pregunta se plantea en muchos menos lugares.

El equipo central de Swift lo ha dicho con esas mismas palabras. En el Swift Group Lab de la WWDC 2026, Holly describió cómo, si diseñara el modelo de concurrencia hoy, las funciones async nonisolated habrían corrido en el contexto de quien llama desde el primer día; parafraseó que se aferró durante mucho tiempo al modelo del pool concurrente global y que los problemas en proyectos reales la convencieron de lo contrario. Doug añadió que el valor por defecto antiguo empujaba demasiados tipos hacia Sendable. (Ambas citas están parafraseadas a partir de una grabación del lab transcrita localmente; Apple no publicó subtítulos para la sesión.)9

Cómo activarlo

Dos ajustes de compilación, ambos en los valores por defecto de Xcode 26 para proyectos nuevos y ambos dignos de configurarse explícitamente en uno ya existente5:

SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor
SWIFT_APPROACHABLE_CONCURRENCY = YES

El primero es SE-0466: el módulo queda por defecto con aislamiento en el main actor. El segundo es el paraguas que habilita el conjunto de funciones de approachable-concurrency, incluido el comportamiento de SE-0461 de “corre en quien llama”. En un paquete de Swift, configuras los mismos valores por defecto a través de swiftSettings con las flags de upcoming-feature correspondientes en lugar del ajuste de compilación de Xcode6.

Activa ambos en un proyecto existente y el recuento de errores cae con fuerza, porque la mayor parte de lo que el verificador marcaba era código del hilo principal que antes no podía asumir que lo fuera. Lo que queda es una lista corta de casos de frontera genuinos. Vale la pena conocerlos por su nombre, porque cada uno es un lugar donde tu código sí abandona el main actor, y el arreglo consiste en decirlo con precisión.

Los seis errores que sobreviven al cambio

Estos son los errores de concurrencia estricta que aún aparecen bajo SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, tomados de migrar las apps de la familia 941 al modelo1. Cada uno es una frontera real, no un falso positivo, y por eso el arreglo es una anotación precisa y no una supresión.

1. Funciones puras sobre tipos Sendable. Un método puro sobre un enum Sendable (un constructor de URL, un formateador) hereda el aislamiento en el main actor bajo el valor por defecto del módulo y luego da error al llamarlo desde un contexto nonisolated: “Call to main actor-isolated instance method in a synchronous nonisolated context.” El método no toca ningún estado, así que aislarlo en el main actor es incorrecto. Márcalo como nonisolated7:

nonisolated func searchURL(for query: String) -> URL? { ... }

2. Statics de singleton en parámetros por defecto. static let shared = Foo() queda aislado en el main actor bajo el valor por defecto, pero los valores de parámetros por defecto se evalúan en el contexto de quien llama, que a menudo es nonisolated: “Main actor-isolated static property ‘shared’ can not be referenced from a nonisolated context.” Haz que el static sea nonisolated. Si el tipo es Sendable (o @unchecked Sendable porque tú mismo proteges su estado), no necesitas ningún calificador unsafe:

final class KeychainProxySecretStore: @unchecked Sendable {
    nonisolated static let shared = KeychainProxySecretStore()
}

3. Constantes primitivas inmutables. El mismo problema del parámetro por defecto golpea a una constante simple: un static let defaultInterval referenciado desde un argumento por defecto nonisolated. El arreglo es idéntico y la constante es trivialmente segura de compartir:

nonisolated static let defaultInterval: TimeInterval = 15 * 60

4. Un cuerpo de Task que lee un self capturado. Una clausura externa captura [weak self]; dentro, un Task { @MainActor in self?.foo() } lee ese opcional capturado: “Reference to captured var ‘self’ in concurrently-executing code.” El Task lee de forma concurrente un binding var del alcance que lo contiene, y ahí está la carrera. Vuelve a capturar self en la frontera del Task para que el Task posea un binding inmutable:

NotificationCenter.default.addObserver(...) { [weak self] _ in
    Task { @MainActor [weak self] in
        self?.value = next
    }
}

5. Callbacks de KVO que leen estado del main actor. Un callback webView.observe(\.canGoBack) { wv, _ in ... } es @Sendable y por tanto nonisolated, pero WKWebView.canGoBack está aislado en el main actor: “Main actor-isolated property ‘canGoBack’ can not be referenced from a Sendable closure.” KVO entrega de forma síncrona en el hilo que mutó el valor, y el estado de navegación de WKWebView muta solo en el hilo principal, así que la lectura es sólida. Asegúralo con MainActor.assumeIsolated, que elimina por completo el salto del Task y se mantiene síncrono8:

let pushNav: @Sendable (WKWebView?) -> Void = { [weak self] webView in
    MainActor.assumeIsolated {
        guard let self else { return }
        // safe to read webView?.canGoBack synchronously
    }
}

assumeIsolated es una promesa al compilador, no una pregunta. Úsalo solo donde el invariante en tiempo de ejecución se cumpla de verdad (un callback documentado en el hilo principal), porque una promesa equivocada es un crash, no una advertencia.

6. Trabajo pesado que no debería estar en el main. Este el verificador no lo marcará, y es el más importante de detectar por tu cuenta. Bajo un valor por defecto en el main actor, un método síncrono de uso intensivo de CPU (decodificar un payload JSON grande, redimensionar una imagen) corre en el main actor y traba tu UI. El valor por defecto te mantiene en el main; @concurrent es cómo lo abandonas a propósito4:

@concurrent
func decodeLargePayload(_ data: Data) async throws -> Report {
    try JSONDecoder().decode(Report.self, from: data)
}

@concurrent descarga la función al ejecutor global y es mutuamente excluyente con @MainActor, con un actor global personalizado y con nonisolated(nonsending), por diseño: una función o corre donde está quien la llama o se aleja de él deliberadamente, nunca de forma ambigua4. La disciplina que pide el nuevo modelo vive enteramente en este patrón. Quédate en el main para todo lo que toque UI, y recurre a @concurrent solo para el trabajo que de forma medible necesite un hilo en segundo plano.

Cuándo los valores por defecto no son los adecuados para ti

El main actor por defecto encaja con las apps: el código de SwiftUI y UIKit es abrumadoramente del hilo principal, y el valor por defecto coincide con la realidad. Encaja peor en dos casos, y fingir lo contrario te hace perder el tiempo.

  • Un target de librería o framework sin UI. Una capa de red, un parser o un motor de datos no tienen razón para quedar por defecto en el main actor, y hacerlo fuerza @concurrent o nonisolated sobre casi todo. Deja SWIFT_DEFAULT_ACTOR_ISOLATION sin configurar para esos targets y aísla de forma deliberada, al revés que antes.
  • Un sistema concurrente con muchos actores. Si tu diseño de verdad ejecuta muchas cosas en paralelo (una tubería real, no una app con unas pocas tareas en segundo plano), el valor por defecto del main actor pelea contra ti. Quieres actors explícitos y código nonisolated, y el valor por defecto de SE-0466 es el punto de partida equivocado.

Para una app, sin embargo, la decisión es fácil: activa ambos ajustes, deja que el recuento de errores se desplome y trata el puñado que queda como un mapa de exactamente dónde tu código abandona el hilo principal. Ese mapa vale la pena tenerlo. El modelo antiguo te daba mil advertencias y ningún mapa; el nuevo te da seis fronteras honestas y un valor por defecto que por fin coincide con cómo corre la app.

Una última nota sobre el ruido frente a la señal. SourceKit mostrará errores de índice entre archivos (“Cannot find type X in scope”, “No such module”) dentro del editor mientras Xcode reconstruye su índice, sobre todo justo después de regenerar un proyecto. Esos son artefactos del índice, no errores de concurrencia. Si xcodebuild reporta BUILD SUCCEEDED, el modelo de concurrencia está satisfecho y el editor solo va con retraso1. Perseguir fantasmas del índice es la forma más rápida de malgastar una tarde en una migración que ya funcionaba.

Preguntas frecuentes

¿Cómo habilito la concurrencia con main actor por defecto en Swift 6.2?

Configura dos ajustes de compilación, ambos activados por defecto en los proyectos nuevos de Xcode 26: SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor y SWIFT_APPROACHABLE_CONCURRENCY = YES5. En un paquete de Swift configuras las flags de upcoming-feature equivalentes a través de swiftSettings en lugar del ajuste de compilación de Xcode6.

¿Qué cambia realmente SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor?

Hace que el código de un módulo quede aislado en el main actor por defecto (SE-0466), de modo que una vista, su modelo y sus helpers son todos @MainActor sin anotaciones2. La concurrencia pasa a ser la excepción con nombre a la que recurres en lugar de la condición ambiental contra la que anotas en cada línea.

¿Qué es @concurrent y cuándo debo usarlo?

@concurrent descarga una función async al ejecutor global, a propósito, lejos del actor de quien la llama4. Bajo un valor por defecto en el main actor lo usas para el trabajo que de forma medible necesita un hilo en segundo plano (decodificar un payload grande, redimensionar una imagen); es mutuamente excluyente con @MainActor, así que una función o corre donde está quien la llama o se aleja de él deliberadamente.

¿Por qué obtengo “Main actor-isolated … can not be referenced from a nonisolated context”?

Bajo el valor por defecto del main actor, las funciones puras, los statics de singleton y las constantes heredan el aislamiento en el main actor y luego dan error al llamarlas desde un contexto nonisolated, como la evaluación de un parámetro por defecto. El arreglo es una anotación nonisolated precisa sobre el miembro, no una supresión; el valor es seguro de compartir porque no toca ningún estado de actor7.

¿Debo usar main actor por defecto para un target de librería o framework?

No. Una capa de red, un parser o un motor de datos no tienen razón para quedar por defecto en el main actor, y hacerlo fuerza @concurrent o nonisolated sobre casi todo. Deja SWIFT_DEFAULT_ACTOR_ISOLATION sin configurar para los targets sin UI y aísla de forma deliberada al revés que antes.

¿Esperar con await una función async nonisolated todavía salta de actor en Swift 6.2?

No. SE-0461 hace que una función async nonisolated corra en el actor que la haya llamado en vez de saltar al ejecutor global, así que esperarla con await ya no te arrastra a través de una frontera de aislamiento ni exige Sendable sobre todo lo que esté en alcance3.



  1. Código de producción del autor en las apps de iOS de la familia 941 (Ki, Return, Get Bananas), todas enviadas con SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor y SWIFT_APPROACHABLE_CONCURRENCY = YES. Los seis patrones de error y sus arreglos de abajo fueron el conjunto completo de limpieza de concurrencia estricta para Ki 1.0.0. 

  2. Swift Evolution, SE-0466: Control default actor isolation inference. Permite que un módulo quede por defecto con aislamiento @MainActor, de modo que los targets de UI y de app corren en el main actor a menos que el código opte por salir mediante @concurrent o un actor explícito. 

  3. Swift Evolution, SE-0461: Run nonisolated async functions on the caller’s actor by default. Una función async nonisolated queda por defecto como nonisolated(nonsending), corriendo en el actor de quien llama en vez de saltar al ejecutor global, lo que elimina los requisitos de Sendable por cruzar la frontera que venían con el salto. 

  4. Swift Evolution, SE-0461 introduce el atributo @concurrent para optar por que una función corra en el ejecutor global (un hilo en segundo plano). @concurrent y nonisolated(nonsending) son los dos modos de aislamiento opuestos para una función async nonisolated: una función o corre donde está quien la llama o se aleja de él deliberadamente. @concurrent no puede combinarse con @MainActor ni con un actor global personalizado. 

  5. SWIFT_APPROACHABLE_CONCURRENCY es el ajuste de compilación paraguas de Xcode que habilita las upcoming features de approachable-concurrency (incluido el comportamiento de SE-0461), y SWIFT_DEFAULT_ACTOR_ISOLATION selecciona el aislamiento por defecto del módulo. Los proyectos nuevos de Xcode 26 habilitan ambos con el valor por defecto del main actor. Documentado en Donny Wals, “Exploring concurrency changes in Swift 6.2”, y Paul Hudson, “What’s new in Swift 6.2”, ambos cruzados con las propuestas subyacentes SE-0461 y SE-0466. 

  6. Swift, Swift Concurrency Migration Guide, “Enabling Complete Concurrency Checking” y configuración del modo de lenguaje. En un paquete de Swift, el aislamiento por defecto y las funciones de approachable-concurrency se configuran a través de las flags de upcoming-feature de swiftSettings en lugar del ajuste de compilación de Xcode. 

  7. Swift, Migration Guide: global actor isolation and nonisolated. Los métodos de un tipo @MainActor heredan el aislamiento en el main actor; nonisolated hace que un método opte por salir, lo cual es correcto para funciones puras que no tocan ningún estado aislado. 

  8. Apple Developer, MainActor.assumeIsolated(_:). Afirma que la ejecución actual ya está en el main actor y corre la clausura de forma síncrona sin un salto de actor. La afirmación produce un trap en tiempo de ejecución si el invariante no se cumple, así que es válida solo donde se garantiza que quien llama está en el hilo principal. 

  9. Apple, WWDC 2026 Swift Group Lab. Las observaciones atribuidas a Holly y Doug del equipo central de Swift están parafraseadas a partir de una grabación transcrita localmente; Apple no publicó transcripción ni subtítulos oficiales de la sesión, así que la redacción es aproximada, no una cita textual. 

Artículos relacionados

Lo que dijo el equipo de Swift de Apple en el lab de la WWDC26

El Swift Group Lab de Apple en la WWDC26 se publicó sin subtítulos. Lo transcribimos en local. Respuestas francas de los…

14 min de lectura

Novedades de Swift (2026): la actualización de la WWDC26

Swift 6.3 y 6.4 de la WWDC26: disponibilidad anyAppleOS, selectores de módulo, accesores borrow/mutate, el protocolo Ite…

19 min de lectura

Dos servidores MCP convirtieron a Claude Code en un sistema de compilación de iOS

XcodeBuildMCP y el MCP de Xcode de Apple le dan a Claude Code acceso estructurado a las compilaciones, pruebas y depurac…

18 min de lectura