La llegada de conceptos como Task, async/await y Actors en Swift ha revolucionado la forma en que escribimos código multihilo. Estas herramientas ofrecen un enfoque mucho más simple y eficiente en comparación con los clásicos DispatchQueue, OperationQueue o Thread.
En este artículo, exploraremos cómo estas nuevas herramientas de concurrencia pueden mejorar drásticamente la calidad y legibilidad de tu código en iOS.
1. El Bloque Task
La unidad básica de trabajo asíncrono en el nuevo modelo de Swift es la Task. Viene a sustituir patrones antiguos como DispatchQueue.main.async {}.
Comparativa: Código antiguo vs. nuevo
Antes, dependíamos de Grand Central Dispatch (GCD) para manejar hilos:
func oldConcurrencyCode() {
DispatchQueue.main.async {
// Ejecución asíncrona en el hilo principal
}
DispatchQueue.global(qos: .background).async {
// Ejecución en segundo plano
}
}
Con la nueva sintaxis, el código es mucho más legible y estructurado:
func newConcurrencyCode() {
Task {
// Se ejecuta asíncronamente (hereda el contexto del actor actual)
}
Task { @MainActor in
// Se ejecuta explícitamente en el hilo principal
}
Task(priority: .background) {
// Se ejecuta con prioridad de segundo plano
}
}
2. Async / Await
Async/await es el corazón de la concurrencia moderna en Swift.
async: Indica que una función o propiedad es asíncrona.await: Se utiliza para esperar el resultado de una operación asíncrona sin bloquear el hilo actual.
Ejemplo práctico: Petición de red
Olvídate de los complejos cierres (closures) y los problemas de pirámides de la perdición (pyramid of doom).
func doSomeRequest() async throws -> MyModel {
// Realiza la petición y lanza error si falla
}
@MainActor
func makesRequestAndUpdateViews() async {
do {
let model = try await doSomeRequest()
view.updateModel(model)
} catch {
view.switchErrorState(error)
}
}
3. Group Operations: Procesamiento en Paralelo
Tradicionalmente, realizar operaciones en grupo requería el uso de DispatchGroup (con sus enter() y leave()), lo cual era propenso a errores. Ahora tenemos TaskGroup.
func readFileInChunks(_ url: URL) async throws -> Data {
try await withThrowingTaskGroup(of: Data.self) { group in
let totalSize = totalSize(url)
for slice in stride(from: .zero, to: totalSize, by: 1_024) {
group.addTask {
return readBytesData(slice, 1_024, url)
}
}
var data = Data()
for try await slice in group {
data.append(slice)
}
return data
}
}
4. Actors: Seguridad de Datos sin Bloqueos Manuales
Los Actors son una herramienta poderosa para proteger el estado de tus objetos. Swift garantiza automáticamente que el acceso a las propiedades y métodos de un actor sea seguro desde múltiples hilos, evitando las «condiciones de carrera» (race conditions).
actor DataManager {
var array: [Int] = []
func append(_ element: Int) {
array.append(element)
}
func read() -> Int? {
array.last
}
}
5. AsyncStream: Flujos de Datos Asíncronos
AsyncStream funciona de forma similar a un array asíncrono, permitiendo producir y consumir elementos bajo demanda mediante el protocolo AsyncSequence.
var closure: ((Int?) -> Void)?
let sequence = AsyncStream<Int> { continuation in
closure = { value in
if let value = value {
continuation.yield(value)
} else {
continuation.finish()
}
}
}
// Consumo de la secuencia
Task {
for await integer in sequence {
print("Recibido: \(integer)")
}
}
Conclusión
La adopción de Swift Concurrency no es solo una cuestión de «estar a la moda», sino de escribir código más seguro, fácil de mantener y con menos errores de sincronización. Si aún usas DispatchQueue, es el momento perfecto para empezar a migrar tus proyectos.