Combine — conceptos clave, Publisher y programación reactiva

Autor: IT Sectr Publicado: 2026-05-03 Tiempo de lectura: 11 min

Combine es el framework nativo de programación reactiva de Apple, presentado en iOS 13, macOS Catalina, tvOS 13 y watchOS 6. Proporciona una API declarativa de Swift para manejar eventos asíncronos mediante el patrón Publisher y Subscriber, reemplazando delegados, closures y NotificationCenter con una cadena unificada. Según Apple, 2025, Combine es la base para SwiftUI y las arquitecturas modernas de iOS, trabajando estrechamente con async/await y Structured Concurrency. El framework está diseñado para la composición de operaciones asíncronas con garantías de seguridad en hilos.

Puntos Clave

  • Combine es el framework reactivo nativo de Apple con tipos Publisher, Subscriber, Subject y tipos de error mediante Failure
  • Publisher emite eventos en el tiempo: valores Output, puede completar con éxito o error Failure
  • Subscriber recibe eventos del Publisher y puede solicitar una cantidad específica de elementos mediante Demand
  • Operadores map, flatMap, filter, combineLatest, zip, debounce transforman flujos sin condiciones de carrera
  • SwiftUI usa Combine mediante @Published, ObservableObject y @StateObject para actualizaciones reactivas de la interfaz

¿Qué es Combine?

Combine es un framework de programación reactiva declarativa integrado en el SDK de Apple. Implementa el patrón Reactive Streams: Publisher produce valores, Subscriber los consume y los operadores transforman el flujo entre ellos. Combine resuelve el problema de los callbacks y delegados proporcionando un modelo de composición unificado para cualquier evento asíncrono — desde respuestas de red hasta cambios en la UI.

Antes de Combine, los desarrolladores de iOS usaban bibliotecas de terceros, principalmente RxSwift. Apple creó Combine como una alternativa nativa con integración profunda en el ecosistema: el framework soporta Objective-C mediante puentes @objc, funciona con KVO (Key-Value Observing a través de NSObject.keyValuePublisher) y NotificationCenter, y también es la base de SwiftUI. Todos los componentes de UIKit publicados en SwiftUI usan Combine internamente para las actualizaciones de vistas.

Combine está diseñado teniendo en cuenta Swift Concurrency: a partir de iOS 15, un Publisher se puede convertir a AsyncSequence mediante .values y usarse en bucles for-await-in. La conversión inversa de funciones async a Publisher se realiza mediante Future. Según Apple WWDC 2024, Combine sigue siendo el framework recomendado para manejar datos en streaming en aplicaciones UIKit, a pesar de la introducción de async/await para llamadas asíncronas puntuales.

Conceptos Fundamentales de Combine

Combine se basa en tres protocolos: Publisher (emite valores de tipo Output, puede fallar con un error de tipo Failure), Subscriber (recibe valores, gestiona Demand — la cantidad de elementos solicitados), Subscription (representa la conexión Publisher-Subscriber con capacidad de cancelación). El canal de datos se inicializa al suscribirse y finaliza al cancelar, completar o con error. Demand es un concepto único de Combine: Subscriber le indica a Publisher cuántos elementos está listo para procesar, implementando contrapresión a nivel de protocolo.

Publisher y Subscriber: Arquitectura de Flujo Reactivo

Publisher es un protocolo con dos tipos asociados: Output (el tipo de valores emitidos) y Failure (el tipo de error que conforma Error). Si el flujo no puede fallar, Failure se especifica como Never — esto garantiza al Subscriber que onReceive solo se llamará con Output. Los Publishers integrados incluyen Just (un solo valor), Sequence (array), URLSession.DataTaskPublisher (solicitud de red), NotificationCenter.Publisher y el property wrapper @Published.

swift
import Combine

// Creando Publisher desde una secuencia
let publisher = [1, 2, 3, 4, 5].publisher

// Creando Subscriber con manejo de valores
class PrintSubscriber: Subscriber {
    typealias Input = Int
    typealias Failure = Never

    func receive(subscription: Subscription) {
        subscription.request(.unlimited)
    }

    func receive(_ input: Int) -> Subscribers.Demand {
        print("Received: \(input)")
        return .unlimited
    }

    func receive(completion: Subscribers.Completion<Never>) {
        print("Completado")
    }
}

publisher.subscribe(PrintSubscriber())

Subscription y Demand

Subscription es un protocolo que representa la conexión activa entre Publisher y Subscriber. El Subscriber recibe Subscription en el método receive(subscription:) y llama a request(_:) para especificar Demand: .unlimited (todos los valores), .max(N) (cantidad limitada) o .none (pausa). Demand puede cambiar dinámicamente — Subscriber puede aumentar o disminuir la cantidad de elementos solicitados durante la recepción de datos. Esto proporciona contrapresión sin almacenamiento en búfer del lado del Publisher.

Subject y CurrentValueSubject

Subject es un tipo que combina Publisher y Subscriber. Un Subject puede usarse como Publisher (los suscriptores se suscriben a él) y simultáneamente como Subscriber (se le envían valores). Combine proporciona dos tipos de Subject: PassthroughSubject (no almacena estado, solo pasa valores nuevos) y CurrentValueSubject (almacena el valor actual y lo pasa a los nuevos suscriptores). Subject es necesario para integrar código imperativo en cadenas reactivas de Combine.

swift
let subject = PassthroughSubject<String, Never>()

// Suscribiéndose como Publisher
let cancellable = subject
    .map { $0.uppercased() }
    .sink { print($0) }

// Enviando valores como Subscriber
subject.send("hello")  // Imprime "HELLO"
subject.send("world")  // Imprime "WORLD"

CurrentValueSubject se diferencia de PassthroughSubject por tener un valor inicial y una propiedad value: un suscriptor recibe inmediatamente el valor actual al suscribirse, y luego todas las actualizaciones posteriores. CurrentValueSubject.value es legible y escribible — cambiar value envía automáticamente el nuevo valor a todos los suscriptores. Esto hace que CurrentValueSubject sea una opción ideal para representar estado en la arquitectura MVVM: ViewModel publica CurrentValueSubject, View se suscribe con cambios mediante sink.

Ambos Subjects pueden terminar el flujo llamando a send(completion: .finished) o send(completion: .failure(error)). Después de la terminación, el Subject deja de aceptar y emitir eventos. Para flujos de larga duración que no deben terminar (por ejemplo, eventos de UI), se recomienda usar PassthroughSubject con Never Failure para evitar llamar accidentalmente a send(completion:).

Operadores de Transformación y Combinación

Operadores de Combine son métodos de Publisher que devuelven un nuevo Publisher. Cada operador crea un nuevo objeto que se suscribe al Publisher upstream y emite valores transformados downstream. Dado que Publisher es un tipo genérico, los operadores mantienen una tipificación estricta: map transforma Output<A> en Output<B>, tryMap agrega la posibilidad de error. Combine contiene alrededor de 100 operadores integrados.

CategoríaOperadorPropósito
Transformaciónmap / tryMap / flatMapTransformar valores o flujos
Filtradofilter / compactMap / removeDuplicatesSeleccionar o limpiar valores
CombinacióncombineLatest / zip / mergeCombinar múltiples Publishers
Control de Tiempodebounce / throttle / delayRetrasar y espaciar eventos
Manejo de Errorescatch / retry / replaceErrorRecuperación después de Failure
Gestión de Demandbuffer / collectAgrupación o almacenamiento en búfer

flatMap en Combine tiene una diferencia importante respecto a la versión de RxSwift: acepta un closure que devuelve un Publisher con el mismo tipo Failure y aplana el Publisher anidado en el flujo principal. flatMap con maxPublishers: .max(1) se comporta como switchMap — cancela el Publisher anidado anterior cuando llega un nuevo valor. Esto es crítico para escenarios de búsqueda: al escribir un nuevo carácter, la solicitud HTTP anterior se cancela automáticamente.

swift
// Búsqueda con debounce y cancelación de la solicitud anterior
searchTextField.textPublisher
    .debounce(for: .seconds(0.3), scheduler: RunLoop.main)
    .removeDuplicates()
    .flatMap(maxPublishers: .max(1)) { query in
        apiService.searchPublisher(query)
            .catch { _ in Just([]) }
    }
    .receive(on: DispatchQueue.main)
    .sink { results in
        self.tableView.reloadData()
    }
    .store(in: &cancellables)

Operadores de combinación — combineLatest y zip — funcionan de manera similar a RxSwift: combineLatest emite una tupla de los últimos valores de todos los Publishers cuando alguno cambia; zip empareja valores por índice. merge combina Publishers del mismo tipo en un solo flujo, no se garantiza la preservación del orden. Combine también tiene select — un operador poco común que elige el primer Publisher que se completa entre varios, y share — transmisión múltiple de un flujo a varios suscriptores sin re-ejecución.

Schedulers y Gestión de Hilos

Scheduler en Combine es un protocolo que define el contexto de ejecución para los operadores. A diferencia de RxSwift con sus más de 5 Schedulers integrados, Combine utiliza mecanismos existentes de Apple: DispatchQueue, RunLoop y OperationQueue. Cada uno de estos tipos cumple con el protocolo Scheduler, lo que permite pasarlos directamente a receive(on:) y subscribe(on:) sin adaptadores adicionales.

receive(on:) cambia el downstream al Scheduler especificado — equivalente a observeOn en RxSwift. Todos los operadores después de receive(on:) se ejecutan en el Scheduler especificado. subscribe(on:) cambia el upstream — afecta la ejecución del Publisher. Un patrón típico: subscribe(on: DispatchQueue.global()) para trabajo en segundo plano y receive(on: DispatchQueue.main) para actualizaciones de UI. En SwiftUI, al usar .onReceive, no se requiere vinculación integrada al hilo principal, pero para sink se recomienda receive(on:).main explícito.

swift
// Carga en segundo plano + UI en el hilo principal
URLSession.shared.dataTaskPublisher(for: url)
    .subscribe(on: DispatchQueue.global(qos: .background))
    .tryMap { data, response -> Data in
        guard let http = response as? HTTPURLResponse,
              http.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }
        return data
    }
    .receive(on: DispatchQueue.main)
    .decode(type: User.self, decoder: JSONDecoder())
    .sink(receiveCompletion: { print($0) },
         receiveValue: { self.nameLabel.text = $0.name })
    .store(in: &cancellables)

RunLoop.main es una alternativa a DispatchQueue.main para operaciones de UI. La diferencia es que RunLoop.main está vinculado al ciclo de eventos actual de la aplicación, mientras que DispatchQueue.main está vinculado a la cola global del hilo principal. Para UIKit se recomienda DispatchQueue.main, para SwiftUI — RunLoop.main. ImmediateWhenScheduler ejecuta operaciones de forma síncrona en el hilo actual — se usa por defecto para pruebas y Publishers simples.

Combine y SwiftUI: Integración mediante ObservableObject

ObservableObject es un protocolo de SwiftUI para objetos que publican cambios. Una clase que implementa ObservableObject puede usar el property wrapper @Published para propiedades cuyos cambios notifican automáticamente a SwiftUI sobre la necesidad de redibujar. Internamente, @Published crea un Publisher que notifica al Publisher objectWillChange cuando wrappedValue cambia. SwiftUI se suscribe a objectWillChange mediante @StateObject, @ObservedObject o @EnvironmentObject.

@Published y @StateObject

@Published es la forma más común de integrar Combine en SwiftUI. Cuando cambia el valor de una propiedad @Published, SwiftUI actualiza todas las Views que usan ese objeto. @StateObject crea una instancia de ObservableObject y se suscribe a sus cambios. Una View creada con @StateObject se redibuja automáticamente cuando cambian las propiedades @Published. Si el objeto debe compartirse entre múltiples Views, se usa @ObservedObject o @EnvironmentObject.

swift
class UserViewModel: ObservableObject {
    @Published var name: String = ""
    @Published var age: Int = 0
    private var cancellables = Set<AnyCancellable>()

    init() {
        $name
            .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
            .sink { [weak self] newName in
                AnalyticsService.logNameChange(newName)
            }
            .store(in: &cancellables)
    }
}

struct UserView: View {
    @StateObject var viewModel = UserViewModel()

    var body: some View {
        TextField("Name", text: $viewModel.name)
    }
}

AnyCancellable es un envoltorio de borrado de tipo para Cancellable que almacena un token de cancelación de suscripción. Set<AnyCancellable> gestiona el ciclo de vida de las suscripciones: cuando el propietario se desinicializa, todos los Cancellable se cancelan automáticamente. En proyectos SwiftUI, Set<AnyCancellable> se declara en la clase ObservableObject, y las suscripciones se agregan mediante .store(in: &cancellables). Para UIKit, se usan los mismos mecanismos con almacenamiento en UIViewController mediante &cancellables o llamadas manuales a cancel().

Combine vs RxSwift

Combine y RxSwift resuelven las mismas tareas de programación reactiva pero tienen diferencias arquitectónicas fundamentales. Combine es parte del SDK de Apple con compatibilidad hacia atrás hasta iOS 13, RxSwift es una biblioteca de terceros compatible con iOS 8+. Combine usa tipificación estricta de errores mediante Failure genérico, RxSwift usa un único tipo Error. Combine está integrado con SwiftUI a nivel de plataforma, RxSwift requiere RxCocoa para extensiones de UI.

La elección entre Combine y RxSwift depende de los requisitos del proyecto. Si la versión mínima de iOS es >= 13 y el proyecto usa SwiftUI — Combine es la opción natural gracias a la integración incorporada y la ausencia de dependencias adicionales. Si el proyecto soporta iOS 11-12, contiene una base de código RxSwift existente o requiere operadores específicos solo disponibles en RxSwift (por ejemplo, Observable.from(path:)), — RxSwift sigue siendo una solución válida.

CaracterísticaCombineRxSwift
DesarrolladorApple (integrado en SDK)ReactiveX (comunidad)
Versión iOSiOS 13+iOS 8+
Tipo de ErrorFailure genérico (Never para UI)Error (cualquiera)
Integración UI@Published + SwiftUIRxCocoa + UIKit
Operadores~100 integrados400+ operadores
Swift ConcurrencyMediante .values (secuencia async)Mediante biblioteca puente

Preguntas Frecuentes

¿Cuál es la diferencia entre PassthroughSubject y CurrentValueSubject?

PassthroughSubject no almacena estado — un suscriptor solo recibe los eventos enviados después de la suscripción. CurrentValueSubject almacena el valor actual y lo pasa a cada nuevo suscriptor inmediatamente al suscribirse. CurrentValueSubject es adecuado para representar estado (por ejemplo, isLoggedIn).

¿Cómo cancelar una suscripción en Combine?

Una suscripción devuelve AnyCancellable, que se cancela al llamar a cancel() o al desinicializarse. Para la gestión grupal, use Set<AnyCancellable> — todas las suscripciones se cancelan al limpiar el conjunto. Esto es análogo a DisposeBag en RxSwift.

¿Necesito aprender Combine después de async/await?

Sí, Combine sigue siendo relevante para datos en streaming: eventos de UI, debounce, combineLatest, WebSocket. async/await es conveniente para solicitudes puntuales, Combine — para flujos continuos o múltiples. Ambos frameworks son complementarios — Publisher se puede convertir a AsyncSequence.

¿Cómo usar Combine con UIKit?

UIKit no tiene Publishers integrados, pero Apple proporciona extensiones: NotificationCenter.default.publisher(for:), Timer.publish, URLSession.dataTaskPublisher. Para eventos de UI personalizados, se usan PassthroughSubject o @IBAction envueltos en un Publisher mediante Future o Subject.

¿Qué es la contrapresión en Combine?

Contrapresión (backpressure) es un mecanismo para controlar la velocidad del flujo: Subscriber le indica a Publisher mediante Demand cuántos elementos está listo para procesar. Si Demand = .max(1), Publisher espera una solicitud antes de enviar el siguiente valor. Esto evita el desbordamiento del búfer cuando las velocidades del productor y consumidor no coinciden.

Resumen

  • Combine es el framework reactivo nativo de Apple para iOS 13+ con el patrón Publisher-Subscriber y tipificación estricta de errores
  • Publisher y Subscriber forman un canal de comunicación con gestión de Demand para contrapresión
  • Subject (Passthrough y CurrentValue) integran código imperativo en cadenas reactivas
  • Operadores map, flatMap, combineLatest, debounce, catch proporcionan procesamiento declarativo de flujos
  • Schedulers mediante DispatchQueue y RunLoop gestionan hilos de ejecución sin bloquear la UI
  • SwiftUI usa Combine mediante @Published, ObservableObject y @StateObject para Views reactivas
  • RxSwift sigue siendo una alternativa para proyectos con iOS 8+, Combine es la elección para nuevos proyectos en iOS 13+

Desarrollaremos una aplicación móvil llave en mano

IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.

Discutir el proyecto

Lea también