Fundamentos de Combine — ¿qué es un Publisher/Subscriber, y dónde sigue siendo relevante junto a async/await?
Combine modela valores que llegan a lo largo del tiempo como un pipeline componible de operadores — y aunque async/await cubre la mayoría del trabajo de una sola vez, sigue siendo la base de @Published, la validación de formularios y la combinación de varias fuentes en vivo.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Fundamentos de Combine — ¿qué es un Publisher/Subscriber, y dónde sigue siendo relevante junto a async/await?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.Combine es el framework de streams reactivos de Apple, lanzado el mismo año que SwiftUI (iOS 13), integrado al SDK sin ninguna dependencia de terceros, y apuntado directamente al mismo problema que RxSwift y ReactiveSwift ya venían resolviendo: modelar valores que llegan a lo largo del tiempo como un pipeline declarativo y componible, en vez de una pirámide de closures de completion handler. Un Publisher emite una secuencia de valores a lo largo del tiempo y opcionalmente se completa o falla; un Subscriber los recibe. Ese es todo el modelo mental, y el resto del framework son operadores para transformar el stream entre esos dos roles.
Formalmente, un Publisher declara un tipo Output y un tipo Failure, y el protocolo de suscripción incluye backpressure: un subscriber pide demanda, el publisher llama a receive(subscription:), después entrega una serie de llamadas receive(_:) con valores, y eventualmente una sola receive(completion:) con .finished o .failure(Error). En la práctica casi nadie implementa este protocolo desde cero — el código de Combine del día a día es componer publishers que otras APIs ya te entregan hechos.
Vale la pena nombrar con precisión esos bloques del día a día porque aparecen todo el tiempo: @Published (que es literalmente cómo funciona por dentro el disparo automático de objectWillChange de ObservableObject — cada propiedad @Published es un publisher de Combine conectado a ese mecanismo), PassthroughSubject y CurrentValueSubject para pipelines manejados a mano, NotificationCenter.default.publisher(for:), Timer.publish, y URLSession.dataTaskPublisher. Sobre esa base, un vocabulario chico de operadores hace el trabajo real: .map, .filter, .debounce(for:scheduler:), .removeDuplicates(), .combineLatest, .merge, .flatMap, .receive(on:) / .subscribe(on:) para saltar de hilo, y finalmente .sink(receiveValue:) o .assign(to:) para consumir el stream de verdad. El valor real de Combine es encadenar varios de estos en un solo pipeline declarativo en vez de anidar completion handlers a mano.
Para el caso común — "hacé un pedido, esperá un valor, listo" — async/await (Swift 5.5+) es directamente la mejor herramienta, y la mayoría del código nuevo en 2025 va primero a eso: no hay que guardar un AnyCancellable con el riesgo de perderle el rastro, no hay que aprender vocabulario de operadores para un solo valor, y la concurrencia estructurada maneja la cancelación sola a través del árbol de tasks. Una cadena que antes se escribía URLSession.dataTaskPublisher(for:).map(\.data).decode(...).sink { }.store(in:&cancellables) se reduce a una sola línea: let (data, _) = try await URLSession.shared.data(for: request).
@MainActorfinal class ProfileLoader: ObservableObject {@Published private(set) var profile: Profile?func load(id: String) async {do {let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api.example.com/profile/\(id)")!)profile = try JSONDecoder().decode(Profile.self, from: data)} catch {profile = nil}}}struct Profile: Decodable {let id: Stringlet name: String}
Combine se sigue ganando su lugar en código de producción en un conjunto específico y más acotado de situaciones. La más clara es combinar más de una fuente viva en un solo valor derivado — un campo de búsqueda que hace debounce del texto ingresado, combina ese término con un publisher separado de un filtro tipo toggle, y cancela automáticamente cualquier pedido que estuviera en curso apenas cambia cualquiera de las dos entradas de nuevo. Eso es un problema de combineLatest + debounce + flatMap, y no se traduce limpiamente a un solo await, porque no hay un único momento para esperar — siguen llegando valores nuevos desde dos fuentes independientes indefinidamente.
import Combineimport Foundationfinal class SearchViewModel: ObservableObject {@Published var query: String = ""@Published private(set) var results: [String] = []private var cancellables = Set<AnyCancellable>()init(searchService: SearchService) {$query.debounce(for: .milliseconds(300), scheduler: RunLoop.main).removeDuplicates().filter { !$0.isEmpty }.flatMap { term insearchService.search(term).catch { _ in Just([]) }}.receive(on: RunLoop.main).assign(to: \.results, on: self).store(in: &cancellables)}}protocol SearchService {func search(_ term: String) -> AnyPublisher<[String], Error>}
Más allá de combinar fuentes, Combine sigue importando porque @Published/ObservableObject está construido sobre él sin importar si el resto del código toca el framework directamente, porque mucho código de interoperabilidad con UIKit escrito entre 2019 y 2023 está basado en Combine y no se va a reescribir solo para modernizar la sintaxis, y porque los streams de eventos genuinamente imperativos y de larga vida — observar NotificationCenter, un Timer que tickea la interfaz de forma repetida — no encajan para nada en la forma de "esperá una vez", porque no hay un único valor que esperar. El AsyncSequence/AsyncStream propio de Swift es la respuesta nativa de async/await a "un stream de muchos valores", y va acotando de a poco el territorio que le queda a Combine, pero la standard library no trae un equivalente propio a combineLatest o debounce — el paquete Swift Async Algorithms cubre parte de esa brecha, de forma despareja.
La señal que un entrevistador realmente está escuchando es precisión, no fidelidad a un bando: poder decir con exactitud dónde gana cada herramienta — una vez contra muchos valores, una sola fuente contra varias combinadas — se lee como mucho más senior que "Combine ya quedó viejo, no lo uses" o "yo uso Combine para todo".