wandres.dev
CONCURRENCIA · async/await y actores

Async streams y flujos de valores

Cuando algo emite valores a lo largo del tiempo —ubicaciones, mensajes, progreso— los async streams te dejan recorrerlos con un bucle for-await, como si fueran una colección.

⏱ 12 min

async/await es perfecto para “pide una cosa, espera una respuesta”. Pero muchas fuentes emiten valores continuamente: la ubicación del GPS, mensajes que llegan, el progreso de una descarga. Para eso están los async streams: una secuencia de valores en el tiempo que recorres con un bucle.

🎯 Al terminar esta lección sabrás
  • Qué es un AsyncSequence.
  • Recorrer valores con for await.
  • Crear tus propios flujos con AsyncStream.
  • Cancelación y ciclo de vida.

Un bucle sobre el tiempo

Un AsyncSequence es como un array cuyos elementos van llegando poco a poco. Lo recorres con for await, que espera cada valor:

for await ubicacion in gestorUbicacion.actualizaciones {
    print("Nueva posición: \(ubicacion)")
    // el bucle continúa cada vez que llega una ubicación nueva
}

El bucle no termina cuando “se acaban los datos ahora”: se pausa esperando el siguiente valor, y sigue cuando llega. Ideal para fuentes vivas.

Crear un flujo con AsyncStream

Envuelve cualquier fuente de eventos (un temporizador, un callback antiguo, un sensor) en un AsyncStream:

func contador() -> AsyncStream<Int> {
    AsyncStream { continuation in
        Task {
            for i in 1...5 {
                try? await Task.sleep(for: .seconds(1))
                continuation.yield(i)        // emite un valor
            }
            continuation.finish()            // termina el flujo
        }
    }
}

// consumirlo:
for await n in contador() {
    print(n)   // 1, 2, 3, 4, 5, uno por segundo
}

yield emite; finish cierra. Cualquier API basada en callbacks o notificaciones puede volverse un stream limpio y recorrible.

El puente entre lo viejo y lo nuevo

AsyncStream es tu herramienta para modernizar código: envuelves una API antigua basada en delegados o closures repetidos, y por fuera ofreces un for await elegante. En vez de esparcir lógica en métodos de callback desconectados, la concentras en un bucle legible. Muchas de las APIs del sistema (ubicación, notificaciones, NotificationCenter) ya ofrecen versiones async así. Reconocer cuándo algo “emite valores en el tiempo” y modelarlo como stream es una señal de madurez con la concurrencia.

Cancelación

Los streams respetan la cancelación de tareas: si la Task que los consume se cancela (por ejemplo, la vista desaparece y su .task se cancela), el bucle for await termina de forma limpia. Por eso .task en SwiftUI y los streams se llevan tan bien:

.task {
    for await evento in fuente.eventos {
        procesar(evento)
    }
    // si la vista desaparece, el bucle se corta solo
}
💡
Comprueba la cancelación en bucles largos

En trabajo largo dentro de una tarea, comprueba Task.isCancelled o llama a try Task.checkCancellation() de vez en cuando para salir pronto si ya no hace falta el resultado. Respetar la cancelación mantiene tu app eficiente: no gastes batería y datos en algo que el usuario ya dejó atrás.

⚔️ Recorre el tiempo
  1. Escribe una función que devuelva un AsyncStream<Int> emitiendo números cada segundo.
  2. Consúmelo con for await e imprime cada valor.
  3. Conéctalo a una vista con .task para actualizar un @State en tiempo real.
  4. Comprueba que, al salir de la vista, el flujo se detiene solo.