Swift 6 supone el cambio más importante en el sistema de tipos del lenguaje desde su lanzamiento: convierte en errores de compilación los data races que Swift 5 solo detectaba con advertencias. El objetivo es que el compilador garantice la ausencia de carreras de datos de la misma forma que garantiza la seguridad de tipos. Este artículo cubre cómo activar la verificación completa, qué errores aparecerán y cómo migrar tu código de forma incremental.
Complete concurrency checking en Swift 5
Antes de saltar a Swift 6, puedes activar la verificación completa en modo advertencia para ver qué afectará a tu código:
// Package.swift
.target(
name: "MiApp",
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
// O en Swift 5.10+:
.unsafeFlags(["-strict-concurrency=complete"])
]
)
En Xcode, puedes activarlo por target en Build Settings > Swift Compiler - Custom Flags: -strict-concurrency=complete.
El modo Swift 6
Swift 6 puede activarse por módulo independientemente de la versión de las dependencias:
// Package.swift
.target(
name: "MiApp",
swiftSettings: [
.swiftLanguageVersion(.v6)
]
)
En Xcode: Build Settings > Swift Language Version > Swift 6.
Errores comunes de data isolation
El error más frecuente al migrar es intentar enviar datos mutables entre contextos de actor distintos:
// ? Swift 6: error 'datos' es de tipo no-Sendable cruzando contextos de actor
@MainActor
class VistaModelo {
var datos: DatosComplejos = DatosComplejos()
func cargar() async {
Task.detached { // Contexto diferente al @MainActor
let resultado = await api.fetch()
await MainActor.run {
self.datos = resultado // ? Error si DatosComplejos no es Sendable
}
}
}
}
La solución es hacer que DatosComplejos conforme a Sendable:
// Opción 1: struct (Sendable por valor semántico)
struct DatosComplejos: Sendable {
var items: [String]
var total: Int
}
// Opción 2: clase final inmutable
final class DatosComplejos: Sendable {
let items: [String] // let, no var
let total: Int
init(items: [String], total: Int) {
self.items = items
self.total = total
}
}
// Opción 3: actor (gestiona su propio aislamiento)
actor DatosComplejos {
var items: [String] = []
var total: Int = 0
}
@preconcurrency: suprimir errores de APIs antiguas
Al integrar SDKs o frameworks que no han adoptado Sendable, puedes usar @preconcurrency para importarlos sin errores de compilación:
@preconcurrency import UIKit // Suprime los errores de Sendable de UIKit
@preconcurrency import LegacySDK
// También para conformancias específicas cuando no controlas el tipo
extension MiClaseLegacy: @unchecked Sendable {}
@unchecked Sendable indica al compilador que confías en que el tipo es seguro entre hilos, pero que la verificación es manual (por ejemplo, porque usas un lock interno).
Typed throws: errores con tipo en Swift 6
Swift 6 introduce los typed throws, que permiten especificar el tipo de error que puede lanzar una función:
// Swift 5: el tipo de error es siempre 'any Error' (existencial)
func parsear(_ json: Data) throws -> Usuario { ... }
// Swift 6: tipo de error específico
enum ParseError: Error {
case jsonInvalido
case campoFaltante(String)
case tipoIncorrecto(campo: String, esperado: String)
}
func parsear(_ json: Data) throws(ParseError) -> Usuario {
guard let dict = try? JSONSerialization.jsonObject(with: json) as? [String: Any] else {
throw ParseError.jsonInvalido
}
guard let nombre = dict["nombre"] as? String else {
throw ParseError.campoFaltante("nombre")
}
return Usuario(nombre: nombre)
}
// Ahora puedes manejar errores sin el cast:
do {
let usuario = try parsear(datos)
} catch .jsonInvalido {
print("JSON malformado")
} catch .campoFaltante(let campo) {
print("Falta el campo: (campo)")
} catch .tipoIncorrecto(let campo, let tipo) {
print("(campo) debería ser (tipo)")
}
Estrategia de migración incremental
Migrar un proyecto grande a Swift 6 de golpe puede ser abrumador. La estrategia recomendada es módulo por módulo:
// 1. Empieza con los módulos más pequeños y sin dependencias
// Package.swift
.target(name: "UtilsCore", swiftSettings: [.swiftLanguageVersion(.v6)]),
.target(name: "Networking", swiftSettings: [.swiftLanguageVersion(.v6)]),
// El módulo principal todavía en Swift 5
.target(name: "MiApp", dependencies: ["UtilsCore", "Networking"]),
Cuando un módulo Swift 6 importa un módulo Swift 5, el compilador aplica @preconcurrency implícitamente a los símbolos del módulo Swift 5, permitiendo la interoperabilidad.
Variables globales y aislamiento
Swift 6 requiere que las variables globales mutables estén aisladas a un actor o sean inmutables:
// ? Swift 6: error variable global mutable sin aislamiento
var configuracionGlobal = Configuracion()
// Opciones válidas:
// 1. Hacer la variable constante
let configuracionGlobal = Configuracion()
// 2. Aislarla a un actor
@MainActor var configuracionGlobal = Configuracion()
// 3. Usar un actor singleton
actor ConfiguracionManager {
static let shared = ConfiguracionManager()
var configuracion = Configuracion()
}
Isolation regions: razonamiento del compilador
Swift 6 introduce el concepto de "isolation regions" para razonar sobre cuándo un valor puede ser enviado de forma segura. El compilador analiza el flujo del programa para determinar si un valor ha "escapado" a otro contexto de aislamiento:
@MainActor
func ejemplo() async {
var datos = DatosNoSendable()
// El compilador analiza que 'datos' no se usa después de este punto
// en el contexto @MainActor, así que puede enviarse de forma segura
await procesarEnBackground(datos)
// No puedes usar 'datos' aquí ya fue enviado
}
Herramientas para verificar la migración
El compilador de Swift proporciona dos modos de diagnóstico para la migración:
// Advertencias sin errores (para empezar a entender el alcance)
-strict-concurrency=targeted // Verificación parcial
-strict-concurrency=complete // Verificación completa como advertencias
// En Xcode, el modo Swift 6 convierte todo en errores
// Usa el "Upgrade to Swift 6" en el asistente de migración
Resumen
Swift 6 no añade nuevas APIs de concurrencia: refuerza las reglas del modelo ya establecido en Swift 5.5-5.9 convirtiéndolas en errores de compilación. El proceso de migración es incremental por diseño: puedes activar la verificación completa como advertencias en Swift 5, después migrar módulo a módulo a Swift 6, y usar @preconcurrency para APIs externas que aún no han adoptado el nuevo modelo. Los typed throws son el otro gran añadido, que hace el manejo de errores más preciso y expresivo.
