← Todos los articulos

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 @MainActor por defecto. Configura SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor y deja de anotar cada vista, modelo y modelo de vista2.
  • SE-0461 hace que las funciones async no 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 de Sendable en la frontera que venían con él3.
  • @concurrent es la válvula 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 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 @concurrent o nonisolated en casi todo. Deja SWIFT_DEFAULT_ACTOR_ISOLATION sin 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.



  1. 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 = 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 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 @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 no aislada adopta por defecto nonisolated(nonsending), ejecutándose en el actor de quien la llama en lugar de saltar al ejecutor global, lo que elimina los requisitos de Sendable por cruzar fronteras que venían con el salto. 

  4. Swift Evolution, SE-0461 introduce el atributo @concurrent para que una función pase a ejecutarse en el ejecutor global (un hilo en segundo plano). @concurrent y nonisolated(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. @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 funciones próximas de concurrencia accesible (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 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. 

  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 concurrencia accesible se configuran mediante las banderas de funciones próximas 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 actor principal; nonisolated excluye un método, 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 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. 

Artículos relacionados

MLX on Apple Silicon: When You Need Your Own Model, Not Apple's

Foundation Models gives you Apple's sealed on-device LLM. MLX runs your own: quantized open-weight models and LoRA fine-…

8 min de lectura

Apple Foundation Models: The On-Device LLM Framework, Explained

Apple's Foundation Models framework: LanguageModelSession, @Generable guided generation, tool calling, availability, and…

11 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