Jodidamente Accesible
Swift Concurrency

Por fin comprende async/await, actors y Sendable. Modelos mentales claros, sin jerga.

Enorme agradecimiento a Matt Massicotte por hacer comprensible la concurrencia en Swift. Recopilado por Pedro Piñera. ¿Encontraste un error? [email protected]

En la tradición de fuckingblocksyntax.com y fuckingifcaseletsyntax.com

Escala tu desarrollo con Tuist

La Verdad Honesta

No existe una chuleta para la concurrencia en Swift. Cada respuesta de "simplemente haz X" está mal en algún contexto.

Pero aquí está la buena noticia: Una vez que entiendas el aislamiento (5 min de lectura), todo encaja. Los errores del compilador empiezan a tener sentido. Dejas de luchar contra el sistema y empiezas a trabajar con él.

Esta guía está orientada a Swift 6+. La mayoría de conceptos aplican a Swift 5.5+, pero Swift 6 aplica una verificación de concurrencia más estricta.

Empieza con el modelo mental ↓

Lo Único Que Necesitas Entender

El aislamiento es la clave de todo. Es la respuesta de Swift a la pregunta: ¿Quién tiene permiso para tocar estos datos ahora mismo?

El Edificio de Oficinas

Piensa en tu app como un edificio de oficinas. Cada oficina es un dominio de aislamiento - un espacio privado donde solo una persona puede trabajar a la vez. No puedes simplemente irrumpir en la oficina de otra persona y empezar a reorganizar su escritorio.

Construiremos sobre esta analogía a lo largo de la guía.

¿Por Qué No Solo Hilos?

Durante décadas, escribimos código concurrente pensando en hilos. ¿El problema? Los hilos no te impiden dispararte en el pie. Dos hilos pueden acceder a los mismos datos simultáneamente, causando data races - bugs que crashean aleatoriamente y son casi imposibles de reproducir.

En un teléfono, quizás te salgas con la tuya. En un servidor manejando miles de peticiones concurrentes, los data races se vuelven una certeza - usualmente apareciendo en producción, un viernes. A medida que Swift se expande a servidores y otros entornos altamente concurrentes, "esperar lo mejor" no es suficiente.

El enfoque antiguo era defensivo: usar locks, dispatch queues, esperar no haber olvidado nada.

El enfoque de Swift es diferente: hacer imposibles los data races en tiempo de compilación. En lugar de preguntar "¿en qué hilo está esto?", Swift pregunta "¿quién tiene permiso para tocar estos datos ahora mismo?" Eso es el aislamiento.

Cómo Otros Lenguajes Manejan Esto

Lenguaje Enfoque Cuándo descubres los bugs
Swift Aislamiento + Sendable Tiempo de compilación
Rust Ownership + borrow checker Tiempo de compilación
Go Channels + detector de races Runtime (con herramientas)
Java/Kotlin synchronized, locks Runtime (crashes)
JavaScript Event loop single-threaded Evitado completamente
C/C++ Locks manuales Runtime (comportamiento indefinido)

Swift y Rust son los únicos lenguajes mainstream que detectan data races en tiempo de compilación. ¿El trade-off? Una curva de aprendizaje más empinada al principio. Pero una vez que entiendes el modelo, el compilador te respalda.

¿Esos molestos errores sobre Sendable y aislamiento de actor? Están detectando bugs que antes habrían sido crashes silenciosos.

Los Dominios de Aislamiento

Ahora que entiendes el aislamiento (oficinas privadas), veamos los diferentes tipos de oficinas en el edificio de Swift.

El Edificio de Oficinas

  • La recepción (MainActor) - donde ocurren todas las interacciones con clientes. Solo hay una, y maneja todo lo que el usuario ve.
  • Oficinas de departamento (actor) - contabilidad, legal, RRHH. Cada departamento tiene su propia oficina protegiendo sus propios datos sensibles.
  • Pasillos y áreas comunes (nonisolated) - espacios compartidos por donde cualquiera puede caminar. Sin datos privados aquí.

MainActor: La Recepción

El MainActor es un dominio de aislamiento especial que se ejecuta en el hilo principal. Es donde ocurre todo el trabajo de UI.

@MainActor
@Observable
class ViewModel {
    var items: [Item] = []  // El estado de UI vive aquí

    func refresh() async {
        let newItems = await fetchItems()
        self.items = newItems  // Seguro - estamos en MainActor
    }
}

En caso de duda, usa MainActor

Para la mayoría de apps, marcar tus ViewModels y clases relacionadas con UI con @MainActor es la elección correcta. Las preocupaciones de rendimiento suelen estar exageradas - empieza aquí, optimiza solo si mides problemas reales.

Actors: Oficinas de Departamento

Un actor es como una oficina de departamento - protege sus propios datos y solo permite un visitante a la vez.

actor BankAccount {
    var balance: Double = 0

    func deposit(_ amount: Double) {
        balance += amount  // ¡Seguro! Solo un llamador a la vez
    }
}

Sin actors, dos hilos leen balance = 100, ambos suman 50, ambos escriben 150 - perdiste $50. Con actors, Swift automáticamente encola el acceso y ambos depósitos se completan correctamente.

No abuses de los actors

Necesitas un actor personalizado solo cuando las cuatro condiciones son verdaderas:

  1. Tienes estado mutable no-Sendable (no thread-safe)
  2. Múltiples lugares necesitan acceder a él
  3. Las operaciones sobre ese estado deben ser atómicas
  4. No puede simplemente vivir en MainActor

Si alguna condición es falsa, probablemente no necesitas un actor. La mayoría del estado de UI puede vivir en @MainActor. Lee más sobre cuándo usar actors.

Nonisolated: Los Pasillos

El código marcado como nonisolated es como los pasillos - no pertenece a ninguna oficina y puede ser accedido desde cualquier lugar.

actor UserSession {
    let userId: String          // Inmutable - seguro de leer desde cualquier lugar
    var lastActivity: Date      // Mutable - necesita protección del actor

    nonisolated var displayId: String {
        "User: \(userId)"       // Solo lee datos inmutables
    }
}

// Uso - no se necesita await para nonisolated
let session = UserSession(userId: "123")
print(session.displayId)  // ¡Funciona síncronamente!

Usa nonisolated para propiedades computadas que solo leen datos inmutables.

Cómo Se Propaga el Aislamiento

Cuando marcas un tipo con un aislamiento de actor, ¿qué pasa con sus métodos? ¿Y con los closures? Entender cómo se propaga el aislamiento es clave para evitar sorpresas.

El Edificio de Oficinas

Cuando te contratan en un departamento, trabajas en la oficina de ese departamento por defecto. Si el departamento de Marketing te contrata, no apareces aleatoriamente en Contabilidad.

De manera similar, cuando una función se define dentro de una clase @MainActor, hereda ese aislamiento. "Trabaja en la misma oficina" que su padre.

Las Clases Heredan Su Aislamiento

@MainActor
class ViewModel {
    var count = 0           // Aislado en MainActor

    func increment() {      // También aislado en MainActor
        count += 1
    }
}

Todo dentro de la clase hereda @MainActor. No necesitas marcar cada método.

Los Tasks Heredan el Contexto (Usualmente)

@MainActor
class ViewModel {
    func doWork() {
        Task {
            // ¡Esto hereda MainActor!
            self.updateUI()  // Seguro, no se necesita await
        }
    }
}

Un Task { } creado desde un contexto @MainActor se queda en MainActor. Esto es usualmente lo que quieres.

Task.detached Rompe la Herencia

@MainActor
class ViewModel {
    func doWork() {
        Task.detached {
            // ¡YA NO está en MainActor!
            await self.updateUI()  // Ahora necesita await
        }
    }
}

El Edificio de Oficinas

Task.detached es como contratar a un contratista externo. No tienen credencial para tu oficina - trabajan desde su propio espacio y deben pasar por canales apropiados para acceder a tus cosas.

Task.detached usualmente está mal

La mayoría del tiempo, quieres un Task regular. Los tasks detached no heredan prioridad, valores task-local, o contexto de actor. Úsalos solo cuando explícitamente necesites esa separación.

Qué Puede Cruzar Fronteras

Ahora que sabes sobre dominios de aislamiento (oficinas) y cómo se propagan, la siguiente pregunta es: ¿qué puedes pasar entre ellos?

El Edificio de Oficinas

No todo puede salir de una oficina:

  • Las fotocopias son seguras de compartir - si Legal hace una copia de un documento y la envía a Contabilidad, ambos tienen su propia copia. Sin conflicto.
  • Los contratos originales firmados deben quedarse donde están - si dos departamentos pudieran modificar el original, se produce el caos.

En términos de Swift: los tipos Sendable son fotocopias (seguras de compartir), los tipos no-Sendable son originales (deben quedarse en una oficina).

Sendable: Seguro para Compartir

Estos tipos pueden cruzar fronteras de aislamiento de forma segura:

// Structs con datos inmutables - como fotocopias
struct User: Sendable {
    let id: Int
    let name: String
}

// Los actors se protegen a sí mismos - manejan sus propios visitantes
actor BankAccount { }  // Automáticamente Sendable

Automáticamente Sendable:

  • Tipos de valor (structs, enums) con propiedades Sendable
  • Actors (se protegen a sí mismos)
  • Clases inmutables (final class con solo propiedades let)

No-Sendable: Deben Quedarse

Estos tipos no pueden cruzar fronteras de forma segura:

// Clases con estado mutable - como documentos originales
class Counter {
    var count = 0  // Dos oficinas modificando esto = desastre
}

¿Por qué es esta la distinción clave? Porque cada error del compilador que encontrarás se reduce a: "Estás intentando enviar un tipo no-Sendable a través de una frontera de aislamiento."

Cuando el Compilador Se Queja

Si Swift dice que algo no es Sendable, tienes opciones:

  1. Hazlo un tipo de valor - usa struct en lugar de class
  2. Aíslalo - mantenlo en @MainActor para que no necesite cruzar
  3. Mantenlo no-Sendable - simplemente no lo pases entre oficinas
  4. Último recurso: @unchecked Sendable - prometes que es seguro (ten cuidado)

Empieza no-Sendable

Matt Massicotte recomienda empezar con tipos regulares, no-Sendable. Añade Sendable solo cuando necesites cruzar fronteras. Un tipo no-Sendable se mantiene simple y evita dolores de cabeza de conformance.

Cómo Cruzar Fronteras

Entiendes los dominios de aislamiento, sabes qué puede cruzarlos. Ahora: ¿cómo te comunicas realmente entre oficinas?

El Edificio de Oficinas

No puedes simplemente irrumpir en otra oficina. Envías una solicitud y esperas una respuesta. Podrías trabajar en otras cosas mientras esperas, pero necesitas esa respuesta antes de poder continuar.

Eso es async/await - enviar una solicitud a otro dominio de aislamiento y pausar hasta obtener una respuesta.

La Palabra Clave await

Cuando llamas a una función en otro actor, necesitas await:

actor DataStore {
    var items: [Item] = []

    func add(_ item: Item) {
        items.append(item)
    }
}

@MainActor
class ViewModel {
    let store = DataStore()

    func addItem(_ item: Item) async {
        await store.add(item)  // Solicitud a otra oficina
        updateUI()             // De vuelta en nuestra oficina
    }
}

El await significa: "Envía esta solicitud y pausa hasta que termine. Podría hacer otro trabajo mientras espero."

Suspensión, No Bloqueo

Concepto Erróneo Común

Muchos desarrolladores asumen que añadir async hace que el código se ejecute en segundo plano. No es así. La palabra clave async solo significa que la función puede pausar. No dice nada sobre dónde se ejecuta.

La clave es la diferencia entre bloqueo y suspensión:

  • Bloqueo: Te sientas en la sala de espera mirando la pared. Nada más pasa.
  • Suspensión: Dejas tu número de teléfono y haces recados. Te llamarán cuando esté listo.
// El hilo está inactivo, sin hacer nada por 5 segundos
Thread.sleep(forTimeInterval: 5)
// El hilo está libre para hacer otro trabajo mientras espera
try await Task.sleep(for: .seconds(5))

Iniciar Trabajo Async desde Código Síncrono

A veces estás en código síncrono y necesitas llamar algo async. Usa Task:

@MainActor
class ViewModel {
    func buttonTapped() {  // Función síncrona
        Task {
            await loadData()  // Ahora podemos usar await
        }
    }
}

El Edificio de Oficinas

Task es como asignar trabajo a un empleado. El empleado maneja la solicitud (incluyendo esperar a otras oficinas) mientras tú continúas con tu trabajo inmediato.

Patrones Que Funcionan

El Patrón de Petición de Red

MainActor Nonisolated (llamada de red)
@MainActor
@Observable
class ViewModel {
    var users: [User] = []
    var isLoading = false

    func fetchUsers() async {
        isLoading = true

        // Esto suspende - el hilo está libre para hacer otro trabajo
        let users = await networkService.getUsers()

        // De vuelta en MainActor automáticamente
        self.users = users
        isLoading = false
    }
}

Sin DispatchQueue.main.async. El atributo @MainActor lo maneja.

Trabajo Paralelo con async let

func loadProfile() async -> Profile {
    async let avatar = loadImage("avatar.jpg")
    async let banner = loadImage("banner.jpg")
    async let details = loadUserDetails()

    // ¡Los tres se ejecutan en paralelo!
    return Profile(
        avatar: await avatar,
        banner: await banner,
        details: await details
    )
}

Prevenir Doble-Taps

Este patrón viene de la guía de Matt Massicotte sobre sistemas con estado:

@MainActor
class ButtonViewModel {
    private var isLoading = false

    func buttonTapped() {
        // Guard SÍNCRONAMENTE antes de cualquier trabajo async
        guard !isLoading else { return }
        isLoading = true

        Task {
            await doExpensiveWork()
            isLoading = false
        }
    }
}

Crítico: El guard debe ser síncrono

Si pones el guard dentro del Task después de un await, hay una ventana donde dos taps de botón pueden ambos iniciar trabajo. Aprende más sobre ordenamiento y concurrencia.

Errores Comunes a Evitar

Estos son errores comunes que incluso desarrolladores experimentados cometen:

Pensar que async = segundo plano

El Edificio de Oficinas

Añadir async no te mueve a una oficina diferente. Sigues en la recepción - solo que ahora puedes esperar entregas sin congelarte en el sitio.

// ¡Esto TODAVÍA bloquea el hilo principal!
@MainActor
func slowFunction() async {
    let result = expensiveCalculation()  // Síncrono = bloqueante
    data = result
}

Si necesitas trabajo hecho en otra oficina, envíalo explícitamente allí:

func slowFunction() async {
    let result = await Task.detached {
        expensiveCalculation()  // Ahora en una oficina diferente
    }.value
    await MainActor.run { data = result }
}

Crear demasiados actors

El Edificio de Oficinas

Crear una nueva oficina para cada pieza de datos significa papeleo interminable para comunicarse entre ellas. La mayoría de tu trabajo puede ocurrir en la recepción.

// Sobre-ingenierizado - cada llamada requiere caminar entre oficinas
actor NetworkManager { }
actor CacheManager { }
actor DataManager { }

// Mejor - la mayoría de cosas pueden vivir en la recepción
@MainActor
class AppState { }

Usar MainActor.run en todas partes

El Edificio de Oficinas

Si sigues caminando a la recepción para cada cosa pequeña, simplemente trabaja allí. Hazlo parte de tu descripción de trabajo, no un recado constante.

// No hagas esto - caminando constantemente a la recepción
await MainActor.run { doMainActorStuff() }

// Haz esto - simplemente trabaja en la recepción
@MainActor func doMainActorStuff() { }

Hacer todo Sendable

No todo necesita ser Sendable. Si estás añadiendo @unchecked Sendable en todas partes, estás haciendo fotocopias de cosas que no necesitan salir de la oficina.

Ignorar warnings del compilador

Cada warning del compilador sobre Sendable es el guardia de seguridad diciéndote que algo no es seguro de llevar entre oficinas. No los ignores - entiéndelos.

Errores Comunes del Compilador

Estos son los mensajes de error reales que verás. Cada uno es el compilador protegiéndote de un data race.

"Sending 'self.foo' risks causing data races"

Sending 'self.foo' risks causing data races

El Edificio de Oficinas

Estás intentando llevar un documento original a otra oficina. O haz una fotocopia (Sendable) o mantenlo en un lugar.

Solución 1: Usa un struct en lugar de una class

Solución 2: Mantenlo en un actor:

@MainActor
class MyClass {
    var foo: SomeType  // Se queda en la recepción
}

"Non-sendable type cannot cross actor boundary"

Non-sendable type 'MyClass' cannot cross actor boundary

El Edificio de Oficinas

Estás intentando llevar un original entre oficinas. El guardia de seguridad te detuvo.

Solución 1: Hazlo una struct:

// Antes: class (no-Sendable)
class User { var name: String }

// Después: struct (Sendable)
struct User: Sendable { let name: String }

Solución 2: Aíslalo en un actor:

@MainActor
class User { var name: String }

"Actor-isolated property cannot be referenced"

Actor-isolated property 'balance' cannot be referenced from the main actor

El Edificio de Oficinas

Estás metiendo la mano en el archivador de otra oficina sin pasar por los canales apropiados.

Solución: Usa await:

// Mal - metiendo la mano directamente
let value = myActor.balance

// Bien - solicitud apropiada
let value = await myActor.balance

"Call to main actor-isolated method in synchronous context"

Call to main actor-isolated instance method 'updateUI()' in a synchronous nonisolated context

El Edificio de Oficinas

Estás intentando usar la recepción sin esperar en la fila.

Solución 1: Haz que el llamador sea @MainActor:

@MainActor
func doSomething() {
    updateUI()  // Mismo aislamiento, no se necesita await
}

Solución 2: Usa await:

func doSomething() async {
    await updateUI()
}

Tres Niveles de Swift Concurrency

No necesitas aprender todo de una vez. Progresa a través de estos niveles:

El Edificio de Oficinas

Piénsalo como hacer crecer una empresa. No empiezas con una sede de 50 pisos - empiezas con un escritorio.

Estos niveles no son límites estrictos - diferentes partes de tu app podrían necesitar diferentes niveles. Una app mayormente-Nivel-1 podría tener una característica que necesita patrones de Nivel 2. Está bien. Usa el enfoque más simple que funcione para cada parte.

Nivel 1: La Startup

Todos trabajan en la recepción. Simple, directo, sin burocracia.

  • Usa async/await para llamadas de red
  • Marca clases de UI con @MainActor
  • Usa el modificador .task de SwiftUI

Esto cubre el 80% de las apps. Apps como Things, Bear, Flighty, o Day One probablemente caen en esta categoría - apps que principalmente obtienen datos y los muestran.

Nivel 2: La Empresa en Crecimiento

Necesitas manejar múltiples cosas a la vez. Es hora de proyectos paralelos y coordinar equipos.

  • Usa async let para trabajo paralelo
  • Usa TaskGroup para paralelismo dinámico
  • Entiende la cancelación de tasks

Apps como Ivory/Ice Cubes (clientes de Mastodon manejando múltiples timelines y actualizaciones en streaming), Overcast (coordinando descargas, reproducción y sincronización en segundo plano), o Slack (mensajería en tiempo real a través de múltiples canales) podrían usar estos patrones para ciertas características.

Nivel 3: La Corporación

Departamentos dedicados con sus propias políticas. Comunicación inter-oficina compleja.

  • Crea actors personalizados para estado compartido
  • Entendimiento profundo de Sendable
  • Executors personalizados

Apps como Xcode, Final Cut Pro, o frameworks Swift del lado del servidor como Vapor y Hummingbird probablemente necesitan estos patrones - estado compartido complejo, miles de conexiones concurrentes, o código a nivel de framework sobre el que otros construyen.

Empieza simple

La mayoría de apps nunca necesitan Nivel 3. No construyas una corporación cuando una startup es suficiente.

Glosario: Más Palabras Clave Que Encontrarás

Más allá de los conceptos básicos, aquí hay otras palabras clave de concurrencia de Swift que verás en el mundo real:

Palabra clave Qué significa
nonisolated Opta por salir del aislamiento de un actor - se ejecuta sin protección
isolated Declara explícitamente que un parámetro se ejecuta en el contexto de un actor
@Sendable Marca un closure como seguro para pasar a través de fronteras de aislamiento
Task.detached Crea un task completamente separado del contexto actual
AsyncSequence Una secuencia que puedes iterar con for await
AsyncStream Una forma de conectar código basado en callbacks con secuencias async
withCheckedContinuation Conecta completion handlers con async/await
Task.isCancelled Verifica si el task actual fue cancelado
@preconcurrency Suprime warnings de concurrencia para código legacy
GlobalActor Protocolo para crear tus propios actors personalizados como MainActor

Cuándo Usar Cada Uno

nonisolated - Leer propiedades computadas

Como una placa con tu nombre en la puerta de tu oficina - cualquiera que pase puede leerla sin necesidad de entrar y esperarte.

Por defecto, todo dentro de un actor está aislado - necesitas await para accederlo. Pero a veces tienes propiedades que son inherentemente seguras de leer: constantes let inmutables, o propiedades computadas que solo derivan valores de otros datos seguros. Marcar estas como nonisolated permite a los llamadores accederlas síncronamente, evitando overhead async innecesario.

Actor-isolated Nonisolated
actor UserSession {
    let userId: String  // Inmutable, seguro de leer
    var lastActivity: Date  // Mutable, necesita protección

    // Esto puede llamarse sin await
    nonisolated var displayId: String {
        "User: \(userId)"  // Solo lee datos inmutables
    }
}
// Uso
let session = UserSession(userId: "123")
print(session.displayId)  // ¡No se necesita await!

@Sendable - Closures que cruzan fronteras

Como un sobre sellado con instrucciones dentro - el sobre puede viajar entre oficinas, y quien lo abra puede seguir las instrucciones de forma segura.

Cuando un closure escapa para ejecutarse más tarde o en un dominio de aislamiento diferente, Swift necesita garantizar que no causará data races. El atributo @Sendable marca closures que son seguros de pasar a través de fronteras - no pueden capturar estado mutable de forma insegura. Swift a menudo infiere esto automáticamente (como con Task.detached), pero a veces necesitas declararlo explícitamente al diseñar APIs que aceptan closures.

@MainActor
class ViewModel {
    var items: [Item] = []

    func processInBackground() {
        Task.detached {
            // Este closure cruza desde el task detached al MainActor
            // Debe ser @Sendable (Swift lo infiere)
            let processed = await self.heavyProcessing()
            await MainActor.run {
                self.items = processed
            }
        }
    }
}

// @Sendable explícito cuando se necesita
func runLater(_ work: @Sendable @escaping () -> Void) {
    DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
        work()
    }
}

withCheckedContinuation - Conectando APIs antiguos

Como un traductor entre el viejo sistema de memorándums de papel y el email moderno. Esperas en el correo hasta que el sistema viejo entrega una respuesta, luego la reenvías a través del nuevo sistema.

Muchas APIs antiguas usan completion handlers en lugar de async/await. En lugar de reescribirlas completamente, puedes envolverlas usando withCheckedContinuation. Esta función suspende el task actual, te da un objeto continuation, y reanuda cuando llamas a continuation.resume(). La variante "checked" detecta errores de programación como resumir dos veces o nunca resumir.

Contexto async Contexto callback
// API vieja basada en callbacks
func fetchUser(id: String, completion: @escaping (User?) -> Void) {
    // ... llamada de red con callback
}

// Envuelta como async
func fetchUser(id: String) async -> User? {
    await withCheckedContinuation { continuation in
        fetchUser(id: id) { user in
            continuation.resume(returning: user)  // ¡Conecta de vuelta!
        }
    }
}

Para funciones que lanzan, usa withCheckedThrowingContinuation:

func fetchUserThrowing(id: String) async throws -> User {
    try await withCheckedThrowingContinuation { continuation in
        fetchUser(id: id) { result in
            switch result {
            case .success(let user):
                continuation.resume(returning: user)
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}

AsyncStream - Conectando fuentes de eventos

Como configurar reenvío de correo - cada vez que llega una carta a la dirección antigua, automáticamente se redirige a tu nuevo buzón. El stream sigue fluyendo mientras siga llegando correo.

Mientras que withCheckedContinuation maneja callbacks de una sola vez, muchas APIs entregan múltiples valores a lo largo del tiempo - métodos de delegado, NotificationCenter, o sistemas de eventos personalizados. AsyncStream conecta estos con AsyncSequence de Swift, permitiéndote usar bucles for await. Creas un stream, guardas su continuation, y llamas a yield() cada vez que llega un nuevo valor.

class LocationTracker: NSObject, CLLocationManagerDelegate {
    private var continuation: AsyncStream<CLLocation>.Continuation?

    var locations: AsyncStream<CLLocation> {
        AsyncStream { continuation in
            self.continuation = continuation
        }
    }

    func locationManager(_ manager: CLLocationManager,
                        didUpdateLocations locations: [CLLocation]) {
        for location in locations {
            continuation?.yield(location)
        }
    }
}

// Uso
let tracker = LocationTracker()
for await location in tracker.locations {
    print("Nueva ubicación: \(location)")
}

Task.isCancelled - Cancelación cooperativa

Como revisar tu bandeja de entrada por un memo de "deja de trabajar en esto" antes de empezar cada paso de un proyecto grande. No te obligan a parar - eliges revisar y responder educadamente.

Swift usa cancelación cooperativa - cuando un task se cancela, no se detiene inmediatamente. En su lugar, se establece una bandera, y es tu responsabilidad verificarla periódicamente. Esto te da control sobre la limpieza y resultados parciales. Usa Task.checkCancellation() para lanzar inmediatamente, o verifica Task.isCancelled cuando quieras manejar la cancelación de forma elegante (como retornando resultados parciales).

func processLargeDataset(_ items: [Item]) async throws -> [Result] {
    var results: [Result] = []

    for item in items {
        // Verifica antes de cada operación costosa
        try Task.checkCancellation()  // Lanza si está cancelado

        // O verifica sin lanzar
        if Task.isCancelled {
            return results  // Retorna resultados parciales
        }

        let result = await process(item)
        results.append(result)
    }

    return results
}

Task.detached - Escapar del contexto actual

Como contratar a un contratista externo que no reporta a tu departamento. Trabajan independientemente, no siguen las reglas de tu oficina, y tienes que coordinarte explícitamente cuando necesitas resultados de vuelta.

Un Task { } regular hereda el contexto actual del actor - si estás en @MainActor, el task se ejecuta en @MainActor. A veces eso no es lo que quieres, especialmente para trabajo intensivo de CPU que bloquearía la UI. Task.detached crea un task sin contexto heredado, ejecutándose en un executor de fondo. Úsalo con moderación - la mayoría del tiempo, Task regular con puntos await apropiados es suficiente y más fácil de razonar.

MainActor Detached
@MainActor
class ImageProcessor {
    func processImage(_ image: UIImage) {
        // NO HAGAS: Esto todavía hereda el contexto de MainActor
        Task {
            let filtered = applyFilters(image)  // ¡Bloquea main!
        }

        // HAZ: Task detached se ejecuta independientemente
        Task.detached(priority: .userInitiated) {
            let filtered = await self.applyFilters(image)
            await MainActor.run {
                self.displayImage(filtered)
            }
        }
    }
}

Task.detached usualmente está mal

La mayoría del tiempo, quieres un Task regular. Los tasks detached no heredan prioridad, valores task-local, o contexto de actor. Úsalos solo cuando explícitamente necesites esa separación.

@preconcurrency - Vivir con código legacy

Silencia warnings al importar módulos que aún no están actualizados para concurrencia:

// Suprime warnings de este import
@preconcurrency import OldFramework

// O en una conformance de protocolo
class MyDelegate: @preconcurrency SomeOldDelegate {
    // No advertirá sobre requisitos no-Sendable
}

@preconcurrency es temporal

Úsalo como puente mientras actualizas código. El objetivo es eventualmente eliminarlo y tener conformance Sendable apropiada.

Lectura Adicional

Esta guía destila los mejores recursos sobre concurrencia en Swift.

Blog de Matt Massicotte (Muy Recomendado)