Novedades en Swift 5.5

Novedades en Swift 5.5

Swift 5.5: Un salto cuántico en la concurrencia

Swift 5.5 marca un antes y un después en el desarrollo de aplicaciones para iOS, macOS, watchOS y tvOS. Esta versión introduce un conjunto de características enfocadas en la concurrencia, lo que permite escribir código más limpio, eficiente y seguro al manejar tareas asíncronas.

¿Qué es la concurrencia?

La concurrencia se refiere a la ejecución simultánea de múltiples tareas. En el contexto de la programación, esto significa que podemos ejecutar varias partes de nuestro código al mismo tiempo, lo que es fundamental para aplicaciones que interactúan con redes, bases de datos o realizan cálculos intensivos.

Novedades clave de Swift 5.5

1. Async/Await: SE-0296

Swift 5.5 sienta las bases para el modelo de concurencia de Swift. Unos de los componentes de ese modelo son asyn/await. Estas palabras del lenguaje permiten marcar funciones/métodos para que se ejecute de manera asíncrona. 

La palabra async convierte una función asíncrona; luego con await esperamos hasta que finalice la ejecución de la función async antes de continuar con la ejecución del código.

Este nuevo enfoque de la concurrencia reemplaza al antiguo modo  de utilizar controladores de finalización(closures). 

Veamos un ejemplo:

				
					import SwiftUI

struct TestAsync : View{
    
    var body: some View{
        VStack{
            Button("Sumar"){
                Task{
                    let result = await suma(opA: 25)
                }
            }
        }
    }
    //Funcion as´íncrona
    func suma(opA : Int) async -> Int{
       return  opA + 1
    }
    
}

//Anteriormente a Swift 5.5 esto solo se podia lograr usando cierres:
struct TestAsync : View{
    
    var body: some View{
        VStack{
            Button("Sumar"){
                    suma(opA: 25) { result in
                      let result = result
                      //El problema aqui es que el código es síncrono y no esperará por el resultado de la función
                    }
            }
        }
    }
    
    func suma(opA : Int, resul : @escaping (Int)->Void){
        DispatchQueue.global().async {
            resul(opA + 1)
        }
    }
}
//nota: Observe con se hace patente la legibilidad y claridad del código
				
			

Cabe señalar que las funciones asíncronas tienen una serie de características:

  • Las funciones asíncronas se ejecutan dentro de un contexto asíncrono. Por esa razón no es posible llamar a una función asíncrona desde un contexto síncrono. Si observa el ejemplo de arriba, notará que hemos envuelto la llamada de la función async en un bloque Task{ }, que crea un contexto asíncrono.
  • Las funciones async pueden llamar a funciones síncronas, si es necesario, además de a otras funciones async.
  •  Si tiene funciones síncronas y asíncronas con el mismo nombre Swift escogerá la función según el contexto. Si este es asíncrono se llamará a la función async, de lo contrario si el contexto de la llamada es síncrono se preferirá la función síncrona (sin async)

Para terminar, resulta útil conocer que async/await se integra muy bien con try/catch. O sea, las funciones async pueden lanzar errores. Hay que tener en cuenta el orden de la palabras. Veamos un ejemplo para ilustrarlo:

				
					enum Errors : Error{
    case divisionPorCero
}

func suma(opA: Int)async throws ->Int{
    if opA <= 0{
        throw Errors.divisionPorCero
    }else{
        return 25 / opA
    }
}

Task{
    do{
        let Result = try await suma(opA: 0)
        print(Result)
    }catch{
        print(error)
    }
}
//Observe como al declarar la función usamos async throws,
//pero al llamarla invertimos las palabras a : try await 
				
			

2. Async sequences: SE-0298

Permite reaalizar bluques sobre secuencias asíncronas de valores utilizando un nuevo protocolo: AsyncSequence.

Esto resulta útil en los lugares en los que se desea procesar valores en una secuencia a medida que están disponibles en lugar de calcularlos todos a la vez, tal vez porque lleva tiempo calcularlos o porque aún no están disponibles.

Consulte esta entrada para más información.

3. Propiedades de solo lectura efectivas:SE-0310

Las propiedades de solo lectura ahora pueden ser marcadas con async y throws. Esto le provee de más flexibilidad y poder. 

Por ejemplo, podríamos tener una estructura que carge el contenido de un archivo. Este contenido podria ser inaccesible, generar un error o simplemenbte ser tan extenso que llevaria tiempo obtenerlo. Para estas situaciones resulta útiles las palabras async/throws. Veamos un ejemplo:

				
					import Foundation

enum FileError: Error {
    case missing, unreadable
}

struct BundleFile {
    let filename: String

    var contents: String {
        get async throws {
            guard let url = Bundle.main.url(forResource: filename, withExtension: nil) else {
                throw FileError.missing
            }

            do {
                return try String(contentsOf: url)
            } catch {
                throw FileError.unreadable
            }
        }
    }
}
//Uso: observe como es necesario usar "try await" para acceder a la variable
func printHighScores() async throws {
    let file = BundleFile(filename: "highscores")
    try await print(file.contents)
}
				
			

4. Concurrencia estructurada: SE-0304

La concurrencia estucturada introdujo una amplia gama de enfoques para ejecutar, cancelar y monitorear operaciones simultáneas en Swift, y se basa en el trabajo introducido por async/await y las secuencias async.

Los principales cambios están dados por dos nuevos tipos: Task y TaskGroup. Que nos permiten ejecutar operaciones concurrentes ya sea individual o coordinada.

El tipo Task permite ejecutar un bloque de código de manera asíncrona. Puede personalizar el modo añadiendo un atributo de prioridad que puede ser: high, default, lowy background. En plataformas Apple puede cambiar  high por userInitiated y utility en lugar de low. El atributo userInteractive solo esta reservada para el hilo principal. 

				
					let task = Task(priority : .high){
 print("work complete!")
}

task
				
			

Los TaskGroup permiten agrupar un conjunto de tareas que trabajan juntas para producir un resultado. 

				
					func test()async {
    
    let taskGroup = await withTaskGroup(of: String.self){ group -> String in
        
        group.addTask {
            "esto es un ejemplo"
        }
        group.addTask {
            let dd = " de lo que podemos hacer"
            return dd
        }
        
        var Result = ""
        
        for await value in group {
            Result.append(value)
        }
        return Result
        
    }
    
    print(taskGroup)
}

Task{
    await test()
}
				
			

5. Enlaces let asíncronos: SE-0317

Swift 5.5 introdujo la capacidad de crear y esperar tareas asíncronas utilizando la sintaxis async let. Esto es una alternativa simple a los grupos de tareas en los que se tranaja con tipos de resultados distintos.

Veamos un ejemplo:

				
					struct Coche{
    let modelo: String
    let pintura : String
    let antiguedad : Int
}
//Ahora creamos tres funciones asíncronas para obtener los resultados

func getModelo()async -> String {
    "Porche"
}
func getPintura()async -> String{
    "Gris mate"
}
func getAntiguedad()async -> Int{
    5
}
//Ahora si queremos crear una instancia de Coche solo tenemos que llamar a cada función con async let de modo que se ejecuten secuencialmente, esperando a que finalice cada una:

func CreateCoche()async{
    async let modelo =  getModelo()
    async let pintura =  getPintura()
    async let Antiguedad =  getAntiguedad()
    
    let coche = await Coche(modelo: modelo, pintura: pintura, antiguedad: Antiguedad)
    print(coche)
}

Task{
    await CreateCoche()
}
				
			

6. Interconectar tareas asíncronas con código síncrono: SE-0300

Se han introducido nuevas funciones para adaptar las API antiguas que utilizaban controladores de finalización al código asíncrono moderlo. 

El uso de un controlador de finalización se ve asi:

				
					func getSuma(opA : Int, completion : @escaping (Int)->Void){
    DispatchQueue.global().async {
        completion(opA + 10)
    }
}
				
			

Si deseas puedes reescribir esta función utilizando las construcciones async/await modernas del lenguaje. Pero puede darse el caso de ese código pertenezca a una librería externa. 

Las continuaciones nos permiten crear un punto medio entre los controladores de finalización y las funciones asincrónocas para que podamos incluir el código anterior en una API más moderna. 

Un ejemplo es la función withCheckedContinuation() que crea una nueva continuación que puede ejecutar cualquier código y luego llamar a resume(returning:) para enviar de vuelta un valor cuando este listo, incluso si forma parte de un cierre de controlador de finalización.

Veamos un ejemplo con la función anterior. Aqui envolvemos la función de la antigua API  en una Continuación y luego usamos esta función en un contexto asíncrónico:

				
					//Envolviendo la función en una Continuación
func getSumaNew(opA : Int)async -> Int{
    await withCheckedContinuation { continuacion in
        getSuma(opA: opA) { result in
            continuacion.resume(returning: result)
        }
    }
}
//Ahora podemos llamar esta función de la forma moderna
func printResult()async{
    let result = await getSumaNew(opA: 25)
    print(result)
}

				
			

Las continuaciones son una solución para adaptar código concurrente antiguo a las formas modernas del lenguaje que manejan funciones asincrónicas. 

Con el uso de withCheckedContinuation() Swift se asegura de realizar comprobaciones en tiempo de ejecución; como la comprobación de si utilizó .resume(returning:) y cuantas veces lo hizo. Esto tiene un coste en el rendimiento. Si desea omitir estas comprobaciones utilice en si lugar withUnsafeContinuation(). Ambas formas son intercambiables por lo que una técnica consiste en utilizar continaciones controladas en fase de desarrollo, donde Swift arrojará errores y advertencias,  y luego cambiar a no controladas antes de liberar el código a producción.

Es importante tener en cuenta que se debe de llamar una sola vez a .resume(returning: ) de lo contrario dará errores inesperados. O sea, debes de reanudar tu continuación exactamente una vez.

7. Actores: SE-0306 

Los Actores son otro tipo de componente esenciales dentro del modelo de concurrencia de Swift. Son tipos por referencia, igual que las clases, que proveen una manera segura de acceder a su estado mutable dentro de un contexto asíncronico. 

Los actores solo permiten que un subproceso pueda acceder a su estado mutable a vez. Esto elimina una serie de errores graves a nivel del compilador.

El concepto aislamiento de actores significa que las propiedades y métodos de un objeto Actor no se pueden acceder desde fuera a menos que se haga de manera asincrónica y el estado mutable de un objeto Actor no se pueden modificar desde fuera del objeto, en absoluto.

El contexto asincrónico que necesita el Actor se debe a que todas las solicitudes de acceso al mismo se colocan en un cola que se ejecuta secuencialmente para evitar carrerras de datos.

Un Actor de define con la palabra actor y comparte muchas similitudes con una Clase como tener propiedades, métodos (que pueden ser async) e inicializadores. También pueden ajustarse a protocolos y ser genéricos. Cualquier propiedad y método que sea estático se comporta de la misma manera porque no poseen el concepto de self. 

Si desea saber más del uso de actores puede leer la siguiente documentación:

8. Actores Globales: SE-0316

Esta novedad permite aislar el estado global de las carreras de datos. La aplicación principal es el uso de @MainActor que puede usarse para marcar propiedades y métodos de modo que estos puedan ser accedidos solamente en el hilo principal. 

Por ejemplo:

				
					//La función save() solo será llamada en el hilo principal
class SaveData {
    @MainActor func save() {
        print("Saving data…")
    }
}

				
			

La notacion @MainActor convierte la propiedad o método como parte de un actor y por lo tanto su acceso solo debe ser asincrónico usando await, async let o similar.

En el ejemplo de arriba la función save() solo prodrá ejecutarse en el hilo principal usando await o async let. @MainActor es en realidad un envoltorio de actor alrededor de una estructura MainActor subyacente y tiene un método run() estático que permite programar el trabajo que se realizará.

9. @Sendables y cierres sendable: SE-0302 

Agrega compatibilidad con datos que se pueden enviar se manera segura entre subprocesos. Esto es posible gracias a un nuevo protocolo Sendable y un atributo @Sendable para funciones.

Existen muchos tipos que pueden ser enviados entre hilos:

  • Todos los tipos principales de Swift, incuidos Bool, Int, String, etc
  • Opcionales, donde los datos envueltos son un tipo de valor
  • Colecciones de la biblioteca estándar que contienen tipo de valores, como Array<String> o Dictionary<Int, String>
  • Tuplas donde los elementos son todos de tipo valor
  • Metatipos como String.self

Todos ellos cumplen con el protocolo Sendable y pueden ser enviados de manera segura entre hilos.

Para más información puede consultar este enlace.

10. #If para expresiones de miembro postfijo: SE-0308

Esto quiere decir que ahora podemos usar el condicional #If para agregar modificadores, opcionalmente,  a una vista en SwiftUI, por ejemplo:

				
					import SwiftUI

Text("Welcome")
#if os(iOS)
    .font(.largeTitle)
#else
    .font(.headline)
#endif
				
			

Esto cambiará el tamaño de la letra según la plataforma. Para más información puede leer aqui.

11. Uso intercambiable de los tipos CGFloat y Double: SE-0307

Ahora swift puede realizar conversiones implícitas entre los tipos CGFloat y Double, en muchos lugares.

12. Codable en enumeraciones con valores asociados: SE-0295

Se actualiza Codable para admitir la escritura de enumeraciones con valores asociados. Anteriormente las enumeraciones solo se admitian si cunplian con el protocolo RawRepresentable, pero ahora se amplia la compatibilidad con enumeraciones generales. 

Por ejemplo:

				
					enum Clima: Codable {
    case Sol
    case Viento(velocidad: Int)
    case Lluvia(cantidad: Int, chance: Int)
}
				
			

Se puede observar un caso simple, un caso con un valor asociado y por último un caso con dos valores asociados.

Ahora podemos crear una matriz que contenga valores sobre el clima. Luego utilizar JSONEncoder para convertir el resultado en una cadena imprimible:

				
					let forecast: [Clima] = [
    .Sol,
    .Viento(velocidad: 13),
    .Lluvia(cantidad: 200, chance: 5)
]

do {
    let result = try JSONEncoder().encode(forecast)
    let jsonString = String(decoding: result, as: UTF8.self)
    print(jsonString)
} catch {
    print("Encoding error: \(error.localizedDescription)")
}
				
			

Swift hará una implementación silenciosa de varios CodingKeys para manejar la estructura anidada que resulta de tener valores adjuntos a los casos de la enumeración. Esto lo podemos hacer a mano, pero Swift hace el trabajo por nosotros.

13. Lazy en contexto locales

Ahora lazy funciona en contextos locales. La palabra clave lazy permite escribir propiedades almacenadas que solo se calculan cuando se utilizan por primera vez. Pero desde Swift 5.5 en adelante podemos usar lazy localmente dentro de una función para crear un comportamiento similar.

Por ejemplo:

				
					func printSaludo(nombre: String)->String{
    print("Esto es un saludo")
    return "Hola \(nombre)!"
}

func testLazyVar(){
    print("Comenzando")
    lazy var saludo = printSaludo(nombre: "Yorj")
    print("Terminando")
    print(saludo)
}

testLazyVar()
//Salida:
/*
Comenzando
Terminando
Esto es un saludo
Hola Yorj!
*/
				
			

14. Extendiendo property wrapper a los parámetros de funciones y cierres:SE-0293

Swift 5.5 extiende los contenedores de propiedades (property wrapper) para que se puedan utilizar con parámetros de funciones y cierres. 

Por ejemplo, podemos escribir un contenedor de propiedad que solo acepte valores en un rango dado:

				
					@propertyWrapper
struct Clamped<T: Comparable> {
    let wrappedValue: T

    init(wrappedValue: T, range: ClosedRange<T>) {
        self.wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound)
    }
}
				
			

Ahora podemos usarlo en un parámetro de función:

				
					func setScore2(@Clamped(range: 0...100) to score: Int) {
    print("Setting score to \(score)")
}

setScore2(to: 50)
setScore2(to: -50)
setScore2(to: 500)
//Salida:
/*
Setting score to 50
Setting score to 0
Setting score to 100
*/
				
			

15. Extendiendo la búsqueda de miembros estáticos en contextos genéricos: SE-0299

Swift permite ahora realizar búsquedas estáticas de miembros de protocolos en contextos genéricos, mejorando la claridad y mantenibilidad del código, particularmente en frameworks como SwiftUI. Un ejemplo lo ilustra mejor:

				
					import SwiftUI

//Antes de Swift 5.5:
Toggle("Example", isOn: .constant(true))
    .toggleStyle(SwitchToggleStyle())
    
// Ahora podemos simplificarlo de este forma:
Toggle("Example", isOn: .constant(true))
    .toggleStyle(.switch)
				
			

Conclusión:

Swift 5.5 es un salto cuántico en la evolución del lenguaje. La concurrencia, antes un tema complejo, se ha vuelto más accesible gracias a async/await. Además, las mejoras en la síntesis de código y otras características hacen que Swift sea una herramienta aún más atractiva para desarrollar aplicaciones de alta calidad. 

Gracias por leer este post, si te ha gustado el contenido dude compartirlo y dejarme un comentario. Feliz codificación!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio