Migración a Swift 6: La estricta concurrencia que debes adoptar

Compartir esta publicación

Cuando se desarrollan aplicaciones, uno de los mayores retos es gestionar múltiples tareas simultáneamente sin que el usuario final note tiempos de espera o, peor aún, errores inesperados. La concurrencia, que permite ejecutar múltiples operaciones simultáneamente, siempre ha sido un tema complejo en el desarrollo. Swift, como lenguaje de programación, ha ido mejorando poco a poco este aspecto, y con la llegada de Swift 6, Apple pretende dar un paso decisivo con la Concurrencia Estricta.

Desde el lanzamiento de Swift en 2014, el lenguaje no ha dejado de evolucionar, haciéndose más seguro y fácil de usar. En Swift 5, async/await fue una importante incorporación que facilitó la escritura de código asíncrono, haciendo que tareas como la descarga de datos o las actualizaciones de la interfaz de usuario fueran más manejables y seguras.

Ahora, con Swift 6 en el horizonte, Apple ha establecido una serie de mejoras y cambios que, aunque no son inmediatamente obligatorios, se convertirán en el estándar a seguir en los próximos años. A partir de abril de 2025, todas las apps que se publiquen en la App Store deberán compilarse con el SDK de iOS 18. Aunque esto no requiere directamente el uso de Swift 6, es un recordatorio de que los desarrolladores deben estar preparados para adoptar las últimas herramientas y características, especialmente en lo que respecta a la concurrencia.

Para muchos, esto significa que migrar a Swift 6 debería ser una prioridad. El cambio no solo mejorará la seguridad y el rendimiento de las apps, sino que también evitará futuros problemas relacionados con la gestión de la concurrencia al aprovechar la potencia de async/await otras mejoras. Si estás trabajando en grandes proyectos o dependes de SDKs de terceros, es esencial que empieces a planificar la transición cuanto antes.

¿Qué es la concurrencia estricta en Swift 6?

Si has estado desarrollando en Swift durante algún tiempo, probablemente hayas oído hablar de la concurrencia y de cómo a veces puede ser un dolor de cabeza, especialmente cuando se gestionan varias tareas simultáneamente. Durante mucho tiempo, hemos estado utilizando herramientas como DispatchQueue o OperationQueue para ejecutar tareas en paralelo. Aunque estas herramientas nos han permitido hacer mucho, también han causado más de un problema cuando algo no funcionaba como se esperaba.

Con Swift 6, Apple nos echa una mano con lo que llaman Concurrencia Estricta. En resumen, nos proporciona reglas más claras y herramientas más seguras para garantizar que el código concurrente no se convierta en un desastre impredecible.

¿Qué significa esto en la práctica?

Antes de Swift 5, si queríamos que parte de nuestro código se ejecutara en paralelo, lanzábamos esas tareas con DispatchQueue o algo similar. Pero esto podía volverse confuso rápidamente y, si no teníamos cuidado, podíamos acabar con condiciones de carrera, accesos inseguros a variables compartidas y todo tipo de problemas que hacían que nuestras aplicaciones fallasen inesperadamente.

En Swift 5, Apple introdujo async/await, una forma más sencilla y limpia de gestionar tareas asíncronas. En lugar de gestionar hilos y colas manualmente, async/await nos permite escribir código que parece secuencial, aunque esté haciendo muchas cosas a la vez.

Ahora, en Swift 6, Apple está presionando aún más para el uso de async/await, y aunque todavía no es estrictamente obligatorio, la idea es que empecemos a adoptarlo como el estándar de facto. Si queremos aprovechar todas las nuevas características de Swift 6, es probable que tengamos que utilizar async/await en gran parte del código.

Hablemos de actores

Otro cambio importante en Swift 6 es la introducción de los actores. ¿Qué son los actores? En pocas palabras, son como pequeñas cajas fuertes que protegen nuestros datos. Imagina que tienes una variable que puede ser modificada por varias tareas simultáneamente. Antes teníamos que asegurarnos manualmente de que dos tareas no accedían a esa variable al mismo tiempo, lo cual era un quebradero de cabeza.

Con los actores, Swift 6 se encarga de eso por nosotros. Los actores aseguran que sólo una tarea pueda modificar una variable a la vez, haciendo que las condiciones de carrera sean cosa del pasado (o al menos mucho menos probables).

He aquí un ejemplo rápido para aclararlo:

Antes de Swift 6, podríamos haber escrito algo como esto para manejar la concurrencia:

swift
actor Counter {
    var value = 0
    
    func increment() {
        value += 1
    }
}

let counter = Counter()

Task {
    await counter.increment()
}

El problema aquí es que si múltiples tareas acceden al counter al mismo tiempo, podríamos acabar con valores incorrectos. Sin embargo, con Swift 6, podemos encapsular ese mismo contador en un actor:

swift
actor Counter {    
var value = 0        
func increment() {        
value += 1    
}
}
let counter = Counter()
Task {    
await counter.increment()}

Este actor garantiza que sólo una tarea a la vez pueda modificar el value, lo que nos ahorra muchos quebraderos de cabeza.

  Nuestra experiencia con Room en Android tras unos meses de uso

Menos errores, más seguridad

Lo que Apple quiere con Strict Concurrency es que dejemos de preocuparnos tanto por esos errores de concurrencia que solían aparecer cuando menos los esperábamos. Al empujarnos a utilizar herramientas como async/await y actors, están haciendo que nuestras aplicaciones sean más seguras y fiables. Y aunque no es algo que tengamos que implementar de inmediato, cuanto antes empecemos a adoptar estas nuevas prácticas, mejor preparados estaremos para el futuro.

Impacto en el código heredado

Migrar un proyecto heredado a Swift 6 con Concurrencia Estricta requiere una serie de cambios que no siempre son obvios a primera vista. Más allá de ajustar algunos hilos o cambiar DispatchQueuea async/await, hay elementos más profundos que pueden causar problemas si no se gestionan adecuadamente. A continuación se detallan algunos aspectos clave a tener en cuenta a la hora de abordar la migración.

Singletons y propiedades globales

Los Singletons y las Propiedades Globales son comunes en los proyectos heredados. Aunque ambos enfoques comparten la idea de que se puede acceder a una única instancia o variable desde cualquier parte del código, también son vulnerables a los problemas de concurrencia. En Swift 6, cualquier acceso compartido a una instancia o propiedad desde múltiples hilos puede causar condiciones de carrera si no se maneja adecuadamente. Anteriormente, podíamos declarar algo como esto:

swift

class NetworkManager {

static let shared = NetworkManager()

private init() {}

func fetchData() {

// Network code

}

}

Este Singleton no estaba protegido contra el acceso concurrente, lo que podía provocar errores difíciles de detectar. Con Concurrencia Estricta, necesitas encapsular tanto Singletons como propiedades globales dentro de actores para garantizar la seguridad en entornos concurrentes.

Migrar a un actor seguro:

swift
actor NetworkManager {
    static let shared = NetworkManager()
    private init() {}

    func fetchData() {
        // Safe network code
    }
}

Los actores garantizan que sólo un hilo pueda acceder a la vez a estos recursos compartidos, lo que elimina la necesidad de gestionar manualmente la sincronización.

Inyección de dependencia

Si su proyecto heredado utiliza un patrón de inyección de dependencias, es crucial revisar cómo se gestionan las instancias inyectadas en un entorno concurrente. En proyectos anteriores, era habitual compartir dependencias entre varios subprocesos sin protección adicional. Sin embargo, con la concurrencia estricta, es necesario acceder de forma segura a las dependencias compartidas.

Un ejemplo sería tener una clase compartida inyectada en varias partes de su código:

swift
class DatabaseManager {
    func saveData() {
        // Save data to database
    }
}

let dbManager = DatabaseManager()

func processData() {
    dbManager.saveData()
}

Este código podría funcionar bien, pero si varias tareas intentan guardar datos simultáneamente, podrían surgir problemas de concurrencia. La solución en Swift 6 sería encapsular la dependencia dentro de un actor para garantizar que las operaciones sean seguras:

swift
actor DatabaseManager {
    func saveData() {
        // Save data safely
    }
}

En proyectos que utilicen inyección de dependencias, asegúrese de revisar todas las clases inyectadas que puedan ser accedidas por múltiples hilos.

Pruebas unitarias y concurrentes

Otro aspecto importante es adaptar las pruebas unitarias para que funcionen con concurrencia. En los proyectos heredados, muchas pruebas probablemente se ejecutan de forma secuencial, sin manejar adecuadamente las operaciones asíncronas. En Swift 6, cualquier prueba que verifique operaciones concurrentes o asíncronas debe actualizarse para utilizar async/await.

Un ejemplo de una prueba concurrente que necesitaría ser refactorizada:

swift
func testCounterIncrement() {
    var counter = 0

    DispatchQueue.global().async {
        counter += 1
    }

    XCTAssertEqual(counter, 1)
}

Esta prueba no es segura bajo concurrencia. Al migrarlo a Swift 6, necesitamos adoptar async/await para asegurar que el código de prueba es seguro:

swift
func testCounterIncrement() async {
    let counter = Counter()
    await counter.increment()

    XCTAssertEqual(await counter.value, 1)
}

Este pequeño cambio no sólo garantiza que la prueba sea segura, sino que también simplifica el flujo de trabajo asíncrono, haciendo que las pruebas sean más predecibles y fiables.

Combinar y async/await

Para los que ya trabajan con Combine en proyectos heredados, una de las ventajas significativas de Swift 6 es la interoperabilidad sin fisuras entre async/await y Combine. Esto significa que puedes seguir utilizando Combine, pero ahora tienes la opción de integrar async/await donde Combine podría haber sido más complejo de manejar.

  Traduciendo texto en JetPack Compose

Por ejemplo, si tienes un pipeline Combine para manejar descargas de datos, podrías refactorizarlo usando async/await para simplificar algunas operaciones:

Antes, con Combine:

swift
let publisher = URLSession.shared.dataTaskPublisher(for: url)
    .map { $0.data }
    .sink(receiveCompletion: { _ in }, receiveValue: { data in
        // Process data
    })

Ahora, con async/await:

swift
func fetchData() async throws -> Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

Aunque Combine sigue siendo una herramienta potente, async/await ofrece una alternativa más directa y legible en muchos casos, especialmente cuando se manejan operaciones asíncronas en código heredado.

Migración de SDK de terceros

Uno de los grandes retos a la hora de migrar un proyecto heredado a Swift 6 con concurrencia estricta es cómo manejar los SDK de terceros. A menudo, estos SDK se diseñaron con métodos de concurrencia más antiguos o simplemente no contemplan las nuevas reglas impuestas por Swift 6. Entonces, ¿qué hacemos cuando el SDK que utilizamos no está preparado para este cambio?

1. Comprueba si se ha actualizado el SDK

Lo primero que tenemos que hacer es comprobar si el proveedor del SDK ha lanzado una nueva versión compatible con Swift 6 y Strict Concurrency. Muchas de las grandes empresas que desarrollan SDKs son conscientes de los cambios que trae Swift, así que es probable que hayan actualizado sus librerías.

Si el SDK ya es compatible con async/await y se ha adaptado para trabajar de forma segura con la concurrencia, la migración será mucho más sencilla.

swift
// SDK updated with async/await
func performAction() async throws {
    let result = try await sdkInstance.someAsyncFunction()
    return result
}

2. Encapsular SDKs que no soportan Concurrencia Estricta

¿Y si el SDK que utilizas no está preparado para Swift 6? Aquí es donde las cosas se complican. Si un SDK todavía depende de DispatchQueue o de métodos de concurrencia manuales, tendrás que hacer algo de trabajo extra para asegurarte de que el uso del SDK es seguro en tu aplicación.

La solución más sencilla es encapsular las llamadas al SDK dentro de un actor. Esto asegura que cualquier acceso al SDK está controlado, incluso si el propio SDK no está gestionando la concurrencia correctamente:

swift
actor SDKWrapper {
    let sdkInstance = SomeThirdPartySDK()

    func performAction() async {
        sdkInstance.action()
    }
}

De este modo, podrás seguir utilizando el SDK sin preocuparse por posibles errores de concurrencia. Sin embargo, esta solución es solo un parche temporal hasta que el SDK sea actualizado oficialmente por sus desarrolladores.

3. Migrar a otro SDK (si es necesario)

En algunos casos, el proveedor del SDK que estás utilizando puede tardar mucho tiempo en actualizar su librería o, peor aún, puede que ya no ofrezca soporte activo. En estos casos, debes evaluar si es posible migrar a otro SDK que ya esté preparado para trabajar con Swift 6 y Strict Concurrency.

Migrar a un nuevo SDK puede ser un quebradero de cabeza, pero a veces es la mejor opción para evitar problemas a largo plazo, sobre todo si el SDK actual está obsoleto o lleno de dependencias que también necesitan actualizarse.

4. Comunicación con el proveedor del SDK

Otro paso importante es ponerse en contacto con el proveedor del SDK si no ves un plan de migración claro. A menudo, las empresas tienen hojas de ruta de actualización o pueden proporcionar información sobre cuándo estará disponible una versión compatible con Swift 6.

Además, estar en contacto con los desarrolladores del SDK también le permite dar su opinión o informar de los problemas que pueda encontrar al intentar adaptar su proyecto a la Concurrencia Estricta.

5. Pruebas y control

Independientemente de si el SDK es compatible o de si has decidido encapsularlo en un actor, es fundamental realizar pruebas exhaustivas. La concurrencia introduce riesgos adicionales que pueden no haber sido evidentes en versiones anteriores de Swift, por lo que tendrás que supervisar de cerca cómo interactúa el SDK con tu aplicación después de la migración.

Por ejemplo, si el SDK gestiona tareas de red o de base de datos de forma asíncrona, es posible que algunos errores de concurrencia sólo se manifiesten en determinadas condiciones de carga. El uso de pruebas unitarias y pruebas de estrés concurrentes es esencial en esta fase.

Ejemplo completo de encapsulación SDK

Supongamos que estamos utilizando un SDK de autenticación que no está adaptado a Swift 6. Podemos encapsularlo en un actor para asegurarnos de que su uso es seguro en nuestro entorno:

swift
actor AuthSDKWrapper {
    let authSDK = LegacyAuthSDK()

    func login(username: String, password: String) async throws -> Bool {
        return try await withCheckedThrowingContinuation { continuation in
            authSDK.login(username: username, password: password) { success, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: success)
                }
            }
        }
    }
}

En este ejemplo, utilizamos withCheckedThrowingContinuation para convertir una API con cierres (común en muchos SDK heredados) en una API basada en async/await, garantizando que todo el flujo sea seguro y predecible.

  Nuestra experiencia migrando de Dagger a Koin

Consejos para la migración gradual a Swift 6 con concurrencia estricta

Migrar a Swift 6 con Concurrencia Estricta no tiene por qué ser un proceso abrumador. Adoptar un enfoque gradual no solo reduce los riesgos, sino que permite a tu equipo aprender y adaptarse a las nuevas reglas con el tiempo. Estos son algunos puntos clave para una transición sin problemas:

1. Abordar primero los módulos críticos

Dé prioridad a los módulos que gestionan tareas asíncronas críticas, como operaciones de red o bases de datos. Estos tienden a ser los más sensibles a los errores de concurrencia y donde async/await puede aportar el beneficio más inmediato. Migrar primero estos componentes esenciales ayudará a estabilizar el sistema desde el principio.

2. Refactorizar gradualmente las dependencias compartidas

Las clases o servicios compartidos por varias partes del sistema, como los gestores de red o de bases de datos, son áreas clave que hay que revisar. Encapsular estas dependencias en actores o adaptar su flujo a async/await ayudará a reducir los riesgos de concurrencia sin alterar el comportamiento esperado.

3. Mantener la compatibilidad con los adaptadores temporales

Si estás trabajando con SDKs o librerías externas que aún no han sido actualizadas a Swift 6, puedes encapsular sus operaciones usando adaptadores. Esto te permitirá seguir migrando sin bloquearte mientras esperas las actualizaciones de terceros.

4. No pases por alto las pruebas

A medida que migra su código, asegurate de que tus pruebas unitarias se actualizan para cumplir con las nuevas reglas de concurrencia. Las pruebas de concurrencia deben evolucionar junto con el código para evitar introducir errores no detectados durante la migración.

5. Forma a tu equipo de desarrollo

Por último, recuerda que migrar a Swift 6 no es solo una cuestión técnica; también implica un cambio en nuestra forma de trabajar. Asegúrate de que tu equipo aprende las nuevas herramientas y paradigmas incorporando revisiones colaborativas y sesiones de formación en torno a async/await y actores.

Recomendaciones

Migrar a Swift 6 no es solo una mejora técnica; es una necesidad inminente para las aplicaciones que quieran seguir cumpliendo las políticas del App Store. A partir de marzo de 2025, todas las aplicaciones nuevas que se envíen tendrán que compilarse con Swift 6, lo que significa que la transición a las nuevas reglas de concurrencia estricta no es opcional, sino un paso inevitable para cualquier equipo de desarrollo.

Un enfoque gradual es clave para evitar que la migración se convierta en una carga innecesaria. Si empiezas ahora, adaptando progresivamente las partes más críticas de tu aplicación, no solo estarás preparado para los requisitos futuros, sino que reducirás el riesgo de encontrarte con problemas de última hora.

Esta es una gran oportunidad para revisar el código heredado, asegurarse de que cumple con los nuevos estándares de concurrencia y mejorar la estabilidad general de la aplicación. Y no olvides que la formación del equipo es igual de crucial: los nuevos paradigmas de concurrencia de Swift 6 requieren un cambio en nuestra forma de pensar y trabajar con código asíncrono.

En resumen, aunque la migración pueda parecer un reto a corto plazo, se trata de una inversión necesaria que le garantizará estar alineado con los estándares futuros al tiempo que construye una base más segura y eficiente para sus aplicaciones.

Referencias

Author

  • Aitor Pagan

    Aitor is an enthusiastic iOS Engineer eager to contribute to team success through hard work, attention to detail and excellent organizational skills. Clear understanding of iOS Platforms and Clean Code Principles and training in Team Management. Motivated to learn, grow and excel in Software Engineering.

    Ver todas las entradas

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Suscríbete a nuestro boletín de noticias

Recibe actualizaciones de los últimos descubrimientos tecnológicos

¿Tienes un proyecto desafiante?

Podemos trabajar juntos

apiumhub software development projects barcelona
Secured By miniOrange