Ingeniería iOS

RxSwift y Combine en iOS: problemas que resuelve la programación reactiva

Al estudiar desarrollo iOS, tarde o temprano encuentras RxSwift o Combine. Aparecen en casi todas las ofertas, pero al abrir el código es fácil desconcertarse ante cadenas de map, flatMap y sink. «Si con cierres y delegados funciona, ¿de verdad tengo que aprender esto?»

6 min de lectura
Imagen de portada de RxSwift y Combine en iOS: problemas que resuelve la programación reactiva

Al estudiar desarrollo iOS, tarde o temprano encuentras RxSwift o Combine. Aparecen en casi todas las ofertas, pero al abrir el código es fácil desconcertarse cuando map, flatMap y sink se encadenan con una sintaxis desconocida. Es natural preguntarse: «Si con cierres y delegados funciona, ¿de verdad tengo que aprender esto?»

Hoy no empezaremos por la sintaxis, sino por por qué surgió la necesidad de estas herramientas. Cuando entiendes el motivo, la sintaxis llega sola.


Las apps iOS son, en realidad, «máquinas de procesar eventos»

Si descompones lo que hace una app, la mayor parte consiste en responder a eventos.

  • El usuario pulsa un botón → cambia la pantalla
  • Llega una respuesta de red → actualiza la lista
  • Aparece el teclado → desplaza el campo de entrada hacia arriba
  • Cambia el valor del campo de texto → vuelve a solicitar los resultados

El problema es que UIKit entrega estos eventos de maneras distintas.

Origen del evento Forma de entrega
Pulsación de botón target-action
Desplazamiento de table view delegate
Respuesta de red completion handler (cierre)
Aparición del teclado NotificationCenter
Cambio de propiedad del objeto KVO(Key-Value Observing)

En iOS es completamente normal usar estas cinco formas en una sola pantalla. La tarea es la misma —responder a un evento—, pero el código queda repartido en cinco formas distintas; para entender una pantalla hay que buscar por todo el view controller.

Ese es el primer problema que intentan resolver RxSwift y Combine. Unifican todos los eventos como un «flujo de valores que llegan a lo largo del tiempo». Pulsaciones, respuestas de red y notificaciones del teclado se manejan con la misma interfaz.


Dos obstáculos que aparecen al resistir con callbacks

Obstáculo 1: Composición de tareas asíncronas

Un requisito habitual es: «Solicitar a la vez los datos del usuario y las publicaciones recientes y dibujar la pantalla cuando lleguen ambos». Con completion handlers, queda así.

var user: User?
var posts: [Post]?

func loadProfile() {
    let group = DispatchGroup()
    group.enter()
    api.fetchUser { result in
        user = try? result.get()
        group.leave()
    }
    group.enter()
    api.fetchPosts { result in
        posts = try? result.get()
        group.leave()
    }
    group.notify(queue: .main) {
        guard let user, let posts else { /* ¿Y dónde se maneja el error?? */ return }
        render(user, posts)
    }
}

Hay que usar DispatchGroup, variables temporales y gestión de errores dispersa. Cuando las solicitudes crecen a tres o cuatro y aparecen dependencias como «cuando termine A, solicita B con su resultado», el anidamiento se vuelve incontrolable. Es el infierno de callbacks.

En Combine, el mismo requisito se expresa así.

api.fetchUser()
    .zip(api.fetchPosts())
    .receive(on: DispatchQueue.main)
    .sink(receiveCompletion: { completion in
        if case .failure(let error) = completion { showError(error) }
    }, receiveValue: { user, posts in
        render(user, posts)
    })
    .store(in: &cancellables)

«Combínalos con zip, recíbelos en el hilo principal y notifica tanto el éxito como el fallo». El código casi se lee igual que el requisito. Además, el manejo de errores queda centralizado.

Obstáculo 2: Control de eventos continuos

Pensemos en un campo de búsqueda. Si llamas a la API con cada pulsación, escribir «Swift» envía cinco solicitudes. Por eso suelen añadirse condiciones como estas.

  • Solicitar solo después de 0,3 segundos sin escribir (debounce)
  • No solicitar si coincide con la búsqueda anterior (eliminar duplicados)
  • Al iniciar una solicitud nueva, cancelar la anterior si aún no terminó

Si lo implementas directamente con Timer y variables de estado, la invalidación del temporizador y la cancelación de solicitudes se entrelazan, creando código propenso a errores. En Combine basta con encadenar operadores probados.

searchTextSubject
    .debounce(for: .seconds(0.3), scheduler: DispatchQueue.main)
    .removeDuplicates()
    .map { api.search(query: $0) }
    .switchToLatest()   // Cancelar automáticamente la solicitud anterior cuando llega una nueva
    .sink { results in render(results) }
    .store(in: &cancellables)

La clave es esta: resolver el control de eventos condicionado por el tiempo ensamblando componentes probados, sin implementarlo directamente. Aquí se ve con mayor claridad el valor de la programación reactiva.

Ensamblar operadores probados en lugar de implementar el control temporal
Ensamblar operadores probados en lugar de implementar el control temporal

Componentes estándar para el binding de MVVM

Como vimos en el artículo anterior, MVVM (Model-View-ViewModel) necesita un binding que actualice automáticamente la View cuando cambia el ViewModel. UIKit no incluye binding integrado. RxSwift (RxCocoa) cubrió ese vacío y, desde iOS 13, Combine asumió ese papel.

viewModel.$isLoading
    .sink { [weak self] in self?.spinner.isAnimating = $0 }
    .store(in: &cancellables)

Esta es la razón práctica por la que tantas ofertas exigen RxSwift o Combine. En los codebases basados en MVVM, la capa de binding está construida, en la práctica, con uno de estos dos frameworks.


RxSwift y Combine: ¿qué diferencia hay?

Conceptualmente, ambos son herramientas de programación reactiva. No es descabellado verlo como un cambio de nombre: Observable pasó a ser Publisher y subscribe pasó a ser sink. Si aprendes uno, puedes pasar al otro con una tabla de correspondencias. Aun así, los criterios de elección son claros.

  • RxSwift: biblioteca de terceros. No tiene restricciones de versión de iOS, ofrece binding abundante para UIKit gracias a RxCocoa y cuenta con mucha documentación y comunidad. A cambio, añade una dependencia externa y aumenta el tiempo de compilación.
  • Combine: framework first-party de Apple. Se puede usar en iOS 13 o posterior sin añadir dependencias y encaja naturalmente con @Published y ObservableObject de SwiftUI. Sin embargo, su soporte de binding para UIKit es más limitado que el de RxCocoa.

Para un proyecto nuevo, Combine suele ser la opción predeterminada más segura. RxSwift se aprende a menudo al incorporarse a un equipo cuyo código existente ya lo utiliza.

RxSwift y Combine son dos cajas de herramientas para lo mismo; la situación del equipo decide
RxSwift y Combine son dos cajas de herramientas para lo mismo; la situación del equipo decide

¿Sigue siendo necesario después de async/await?

Desde async/await en Swift 5.5 se pregunta a menudo: «¿Ya no hace falta aprender Combine?». La respuesta es cierta a medias. Para la asincronía puntual «una solicitud, una respuesta», async/await se lee mucho mejor. El ejemplo del perfil anterior solo requiere async let dos líneas.

Pero los flujos de valores que llegan continuamente, como el campo de búsqueda, son otra historia. Si hay que aplicar controles temporales como debounce o combineLatest a eventos continuos —entrada de texto, actualizaciones de ubicación, mensajes de WebSocket o cambios de estado del ViewModel—, las herramientas reactivas siguen siendo adecuadas. Apple también amplía este ámbito con AsyncSequence, pero el ecosistema de operadores aún es más completo en Combine.

Se resume así.

  • Asincronía puntual (por ejemplo, una solicitud de red) → async/await
  • Flujos de eventos continuos + control temporal y binding de UI → Combine (o RxSwift)

Más que competir, son herramientas con ámbitos de responsabilidad distintos.


Conclusión

En una frase, usamos RxSwift y Combine para unificar eventos asíncronos heterogéneos en una interfaz de flujo y tratar declarativamente la composición, el control temporal y el binding sobre ella.

  • Unificar en un solo estilo el manejo de eventos disperso entre delegados, cierres y notificaciones
  • Expresar la composición de varias tareas asíncronas sin callbacks anidados
  • Resolver con operadores probados controles temporales como debounce, eliminación de duplicados y cancelación de solicitudes
  • Componente de facto estándar para el binding de MVVM

La sintaxis parece intimidante al principio, pero al adoptar la idea de «ver los eventos como flujos de valores», los operadores no son más que map y filter de los arrays extendidos en el eje temporal. En el próximo artículo analizaremos cómo Publisher y Subscriber de Combine encajan y funcionan internamente.

Lectura recomendada