Concurrencia en Swift 6.2 en la práctica: usa MainActor por defecto, sal de él a propósito
The “current translated body” is not a translation at all — it’s a previous agent’s commentary log that overwrote the actual content, which is why every structural check fails (no headings, no code fences, no URLs, no citations). It must be rebuilt as a true Spanish translation of the source. Here is the complete corrected Markdown body:
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ó código de interfaz corriente en un muro de violaciones de Sendable y errores de “main actor-isolated property can not be referenced from a nonisolated context”. El diagnóstico era correcto (ese código de verdad podía sufrir carreras), pero el volumen sepultó la señal, y muchos equipos se quedaron en el modo de Swift 5 o salpicaron @MainActor hasta que los errores se callaron.
Swift 6.2 cambia los valores 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: la mayoría de tu código se ejecuta en el actor principal, y lo abandonas a propósito, en lugares con nombre. Este es el modelo que despliego en las apps de la familia 9411. Así funciona, y estos son los seis errores que aún muerden una vez que lo activas.
TL;DR
- SE-0466 te permite poner todo un módulo en
@MainActorpor defecto. ConfiguraSWIFT_DEFAULT_ACTOR_ISOLATION = MainActory deja de anotar cada vista, modelo y modelo de vista2. - SE-0461 hace que las funciones
asyncno aisladas se ejecuten en el actor de quien las llama por defecto (nonisolated(nonsending)), de modo que llamar a código asíncrono ya no fuerza un salto de actor ni los errores deSendableen la frontera que venían con él3. @concurrentes la válvula de escape. Marca una función como@concurrentpara 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 general es
SWIFT_APPROACHABLE_CONCURRENCY = YES5. - El modelo invierte la carga: en lugar de demostrar que cada línea es segura para ejecutarse fuera del hilo principal, mantienes todo en el hilo principal y demuestras los pocos lugares que abandonas.
- Seis errores concretos sobreviven al cambio. Los seis tienen arreglos de una línea una vez que ves el patrón.
La inversión, y por qué importa
Modelo antiguo (Swift 6.0): el código no está aislado hasta que tú lo aíslas. Cada tipo que tocaba el estado de la interfaz necesitaba @MainActor, cada llamada asíncrona que cruzaba una frontera de aislamiento necesitaba conformidad con Sendable, y el compilador señalaba cada hueco. Para una app donde el 95 por ciento del código ya se ejecuta 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 actor principal hasta que lo abandonas. SE-0466 te permite declarar el aislamiento en el actor principal como valor por defecto del módulo, de modo que una vista, su modelo y sus auxiliares son todos @MainActor sin una sola anotación2. SE-0461 elimina entonces el segundo impuesto: una función async no aislada ahora se ejecuta en el actor que la llamó en lugar de saltar al ejecutor global, así que esperarla con await no te arrastra a través de una frontera de aislamiento ni exige Sendable en todo lo que esté a la vista3.
El modelo mental es el que coincide con cómo se comportan realmente las apps de interfaz. El hilo principal por defecto no es un compromiso; es la verdad de una app cuyo estado son sus vistas. La concurrencia se convierte en 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 ejecutarse de forma concurrente” a “dijiste que esto se ejecuta de forma concurrente, así que demuestra que es seguro”, y la segunda pregunta se plantea en muchos menos lugares.
Cómo activarlo
Dos ajustes de compilación, ambos entre 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 adopta por defecto el aislamiento en el actor principal. El segundo es el paraguas que habilita el conjunto de funciones de concurrencia accesible, incluido el comportamiento de SE-0461 en el que ejecuta quien llama. En un paquete de Swift, configuras los mismos valores por defecto mediante swiftSettings con las banderas de funciones próximas correspondientes en lugar del ajuste de compilación de Xcode6.
Activa ambos en un proyecto existente y el número de errores cae con fuerza, porque la mayor parte de lo que el verificador señalaba era código del hilo principal que antes no podía suponer que fuera del hilo principal. Lo que queda es una lista corta de casos frontera genuinos. Vale la pena conocerlos por su nombre, porque cada uno es un lugar donde tu código de verdad abandona el actor principal, 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 en lugar de 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 actor principal bajo el valor por defecto del módulo, y luego falla al llamarse desde un contexto no aislado: “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 actor principal es incorrecto. Márcalo como nonisolated7:
nonisolated func searchURL(for query: String) -> URL? { ... }
2. Estáticos de singleton en parámetros por defecto. static let shared = Foo() está aislado en el actor principal 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 no está aislado: “Main actor-isolated static property ‘shared’ can not be referenced from a nonisolated context.” Haz el estático nonisolated. Si el tipo es Sendable (o @unchecked Sendable porque proteges su estado tú mismo), no necesitas ningún calificador inseguro:
final class KeychainProxySecretStore: @unchecked Sendable {
nonisolated static let shared = KeychainProxySecretStore()
}
3. Constantes primitivas inmutables. El mismo problema de parámetros por defecto golpea a una constante simple: un static let defaultInterval referenciado desde un argumento por defecto no aislado. El arreglo es idéntico y la constante es trivialmente segura de compartir:
nonisolated static let defaultInterval: TimeInterval = 15 * 60
4. El cuerpo de una Task que lee un self capturado. Un cierre exterior captura [weak self]; dentro, una Task { @MainActor in self?.foo() } lee ese opcional capturado: “Reference to captured var ‘self’ in concurrently-executing code.” La Task lee un enlace var del ámbito envolvente de forma concurrente, y eso es la carrera. Vuelve a capturar self en la frontera de la Task para que la Task posea un enlace inmutable:
NotificationCenter.default.addObserver(...) { [weak self] _ in
Task { @MainActor [weak self] in
self?.value = next
}
}
5. Callbacks de KVO que leen estado del actor principal. Un callback webView.observe(\.canGoBack) { wv, _ in ... } es @Sendable y, por tanto, no aislado, pero WKWebView.canGoBack está aislado en el actor principal: “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 solo muta en el hilo principal, así que la lectura es sólida. Afírmalo con MainActor.assumeIsolated, que elimina por completo el salto de 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 genuinamente (un callback documentado del hilo principal), porque una promesa equivocada es un fallo grave, no una advertencia.
6. Trabajo pesado que no debería estar en el hilo principal. Este el verificador no lo señalará, y es el más importante de detectar por tu cuenta. Bajo un valor por defecto en el actor principal, un método síncrono limitado por CPU (decodificar un JSON de gran tamaño, redimensionar una imagen) se ejecuta en el actor principal y entrecorta tu interfaz. El valor por defecto te mantiene en el hilo principal; @concurrent es como 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 se ejecuta donde está quien la llama o deliberadamente huye de él, nunca de forma ambigua4. La disciplina que pide el nuevo modelo vive por completo en este patrón. Quédate en el hilo principal para todo lo que toque la interfaz, y recurre a @concurrent solo para el trabajo que, de forma medible, necesita un hilo en segundo plano.
Cuándo los valores por defecto son inadecuados para ti
El aislamiento en el actor principal 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 malgasta tu tiempo.
- Un objetivo de biblioteca o framework sin interfaz. Una capa de red, un analizador o un motor de datos no tiene razón para adoptar por defecto el actor principal, y hacerlo fuerza
@concurrentononisolateden casi todo. DejaSWIFT_DEFAULT_ACTOR_ISOLATIONsin configurar para esos objetivos y aísla de forma deliberada, a la manera antigua. - Un sistema concurrente con muchos actores. Si tu diseño de verdad ejecuta muchas cosas en paralelo (una canalización real, no una app con unas pocas tareas en segundo plano), el valor por defecto del actor principal te combate. Quieres actores explícitos y código no aislado, y el valor por defecto de SE-0466 es el punto de partida equivocado.
Para una app, en cambio, la decisión es fácil: activa ambos ajustes, deja que el número de errores se desplome, y trata al puñado que queda como un mapa de exactamente dónde tu código abandona el hilo principal. Ese mapa vale la pena. 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 se ejecuta la app.
Una última nota sobre ruido frente a 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 informa 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.
-
Código de producción del autor en las apps iOS de la familia 941 (Ki, Return, Get Bananas), todas en producción con
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActorySWIFT_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. ↩↩↩ -
Swift Evolution, SE-0466: Control default actor isolation inference. Permite que un módulo adopte por defecto el aislamiento
@MainActor, de modo que los objetivos de interfaz y de app se ejecuten en el actor principal a menos que el código se excluya mediante@concurrento un actor explícito. ↩↩ -
Swift Evolution, SE-0461: Run nonisolated async functions on the caller’s actor by default. Una función
asyncno aislada adopta por defectononisolated(nonsending), ejecutándose en el actor de quien la llama en lugar de saltar al ejecutor global, lo que elimina los requisitos deSendablepor cruzar fronteras que venían con el salto. ↩↩ -
Swift Evolution, SE-0461 introduce el atributo
@concurrentpara que una función pase a ejecutarse en el ejecutor global (un hilo en segundo plano).@concurrentynonisolated(nonsending)son los dos modos de aislamiento opuestos para una función asíncrona no aislada: una función o se ejecuta donde está quien la llama o deliberadamente huye de él.@concurrentno puede combinarse con@MainActorni con un actor global personalizado. ↩↩↩ -
SWIFT_APPROACHABLE_CONCURRENCYes el ajuste de compilación paraguas de Xcode que habilita las funciones próximas de concurrencia accesible (incluido el comportamiento de SE-0461), ySWIFT_DEFAULT_ACTOR_ISOLATIONselecciona el aislamiento por defecto del módulo. Los proyectos nuevos de Xcode 26 habilitan ambos con el valor por defecto del actor principal. Documentado en Donny Wals, “Exploring concurrency changes in Swift 6.2”, y Paul Hudson, “What’s new in Swift 6.2”, ambos contrastados con las propuestas subyacentes SE-0461 y SE-0466. ↩↩ -
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 concurrencia accesible se configuran mediante las banderas de funciones próximas de
swiftSettingsen lugar del ajuste de compilación de Xcode. ↩ -
Swift, Migration Guide: global actor isolation and
nonisolated. Los métodos de un tipo@MainActorheredan el aislamiento en el actor principal;nonisolatedexcluye un método, lo cual es correcto para funciones puras que no tocan ningún estado aislado. ↩ -
Apple Developer,
MainActor.assumeIsolated(_:). Afirma que la ejecución actual ya está en el actor principal y ejecuta el cierre de forma síncrona sin un salto de actor. La afirmación produce un fallo grave en tiempo de ejecución si el invariante no se cumple, así que solo es válida donde se garantiza que quien llama está en el hilo principal. ↩