Combine é o framework nativo de programação reativa da Apple, apresentado no iOS 13, macOS Catalina, tvOS 13 e watchOS 6. Ele fornece uma API declarativa Swift para processar eventos assíncronos através do padrão Publisher e Subscriber, substituindo delegates, closures e NotificationCenter por uma cadeia unificada. De acordo com a Apple, 2025, o Combine é a base para o SwiftUI e arquiteturas modernas do iOS, trabalhando em estreita colaboração com async/await e Structured Concurrency. O framework foi projetado para composição de operações assíncronas com garantias de segurança de threads.
Principais Conclusões
Combine é um framework de programação reativa declarativa integrado ao SDK da Apple. Ele implementa o padrão Reactive Streams: Publisher produz valores, Subscriber os consome e os operadores transformam o fluxo entre eles. O Combine resolve o problema de callbacks e delegates fornecendo um modelo de composição unificado para qualquer evento assíncrono — desde respostas de rede até mudanças na interface do usuário.
Antes do Combine, os desenvolvedores iOS usavam bibliotecas de terceiros, principalmente o RxSwift. A Apple criou o Combine como uma alternativa nativa com integração profunda no ecossistema: o framework suporta Objective-C através de pontes @objc, funciona com KVO (Key-Value Observing via NSObject.keyValuePublisher) e NotificationCenter, e também é a base para o SwiftUI. Todos os componentes UIKit publicados no SwiftUI usam o Combine internamente para atualizações de visualização.
O Combine é projetado com a Swift Concurrency em mente: a partir do iOS 15, um Publisher pode ser convertido em AsyncSequence através de .values e usado em loops for-await-in. A conversão reversa de funções async para Publisher é feita através de Future. De acordo com a Apple WWDC 2024, o Combine continua sendo o framework recomendado para manipulação de dados em streaming em aplicativos UIKit, apesar da introdução de async/await para chamadas assíncronas únicas.
O Combine é baseado em três protocolos: Publisher (emite valores do tipo Output, pode falhar com um erro do tipo Failure), Subscriber (recebe valores, gerencia Demand — o número de elementos solicitados), Subscription (representa a conexão Publisher-Subscriber com capacidade de cancelamento). O canal de dados é inicializado ao assinar e termina ao cancelar, completar ou com erro. Demand é um conceito único do Combine: o Subscriber informa ao Publisher quantos elementos está pronto para processar, implementando backpressure no nível do protocolo.
Publisher é um protocolo com dois tipos associados: Output (o tipo de valores emitidos) e Failure (o tipo de erro que implementa Error). Se o fluxo não pode falhar, Failure é especificado como Never — isso garante ao Subscriber que onReceive será chamado apenas com Output. Publishers integrados incluem Just (valor único), Sequence (array), URLSession.DataTaskPublisher (requisição de rede), NotificationCenter.Publisher e o property wrapper @Published.
import Combine
// Criando Publisher a partir de uma sequência
let publisher = [1, 2, 3, 4, 5].publisher
// Criando Subscriber com tratamento 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("Concluído")
}
}
publisher.subscribe(PrintSubscriber())
Subscription é um protocolo que representa a conexão ativa entre Publisher e Subscriber. O Subscriber recebe Subscription no método receive(subscription:) e chama request(_:) para especificar Demand: .unlimited (todos os valores), .max(N) (quantidade limitada) ou .none (pausa). O Demand pode mudar dinamicamente — o Subscriber pode aumentar ou diminuir o número de elementos solicitados durante o recebimento de dados. Isso fornece backpressure sem buffer no lado do Publisher.
Subject é um tipo que combina Publisher e Subscriber. Um Subject pode ser usado como Publisher (assinantes assinam ele) e simultaneamente como Subscriber (valores são enviados para ele). O Combine fornece dois tipos de Subject: PassthroughSubject (não armazena estado, apenas passa novos valores) e CurrentValueSubject (armazena o valor atual e o passa para novos assinantes). O Subject é necessário para integrar código imperativo em cadeias reativas do Combine.
let subject = PassthroughSubject<String, Never>()
// Assinando 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 difere do PassthroughSubject por ter um valor inicial e uma propriedade value: um assinante recebe imediatamente o valor atual ao assinar, e então todas as atualizações subsequentes. CurrentValueSubject.value é legível e gravável — alterar value envia automaticamente o novo valor para todos os assinantes. Isso torna o CurrentValueSubject uma escolha ideal para representar estado na arquitetura MVVM: ViewModel publica CurrentValueSubject, View assina com mudanças através de sink.
Ambos os Subjects podem encerrar o fluxo chamando send(completion: .finished) ou send(completion: .failure(error)). Após o encerramento, o Subject para de aceitar e emitir eventos. Para fluxos de longa duração que não devem ser encerrados (por exemplo, eventos de UI), recomenda-se usar PassthroughSubject com Never Failure para evitar chamar acidentalmente send(completion:).
Operadores do Combine são métodos do Publisher que retornam um novo Publisher. Cada operador cria um novo objeto que assina o Publisher upstream e emite valores transformados downstream. Como Publisher é um tipo genérico, os operadores mantêm tipagem estrita: map transforma Output<A> em Output<B>, tryMap adiciona a possibilidade de erro. O Combine contém cerca de 100 operadores integrados.
| Categoria | Operador | Propósito |
|---|---|---|
| Transformação | map / tryMap / flatMap | Transformar valores ou fluxos |
| Filtragem | filter / compactMap / removeDuplicates | Selecionar ou limpar valores |
| Combinação | combineLatest / zip / merge | Combinar vários Publishers |
| Controle de Tempo | debounce / throttle / delay | Atrasar e espaçar eventos |
| Tratamento de Erros | catch / retry / replaceError | Recuperação após Failure |
| Gerenciamento de Demand | buffer / collect | Agrupamento ou buffer |
flatMap no Combine tem uma diferença importante em relação à versão do RxSwift: ele aceita uma closure que retorna um Publisher com o mesmo tipo Failure e achata o Publisher aninhado no fluxo principal. flatMap com maxPublishers: .max(1) se comporta como switchMap — cancela o Publisher aninhado anterior quando um novo valor chega. Isso é crítico para cenários de busca: quando um novo caractere é digitado, a requisição HTTP anterior é cancelada automaticamente.
// Busca com debounce e cancelamento da requisição 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 combinação — combineLatest e zip — funcionam de forma similar ao RxSwift: combineLatest emite uma tupla dos valores mais recentes de todos os Publishers quando qualquer um muda; zip combina valores por índice. merge combina Publishers do mesmo tipo em um único fluxo, a preservação de ordem não é garantida. O Combine também tem select — um operador raro que escolhe o primeiro Publisher a completar entre vários, e share — multicast de um fluxo para múltiplos assinantes sem re-execução.
Scheduler no Combine é um protocolo que define o contexto de execução para operadores. Ao contrário do RxSwift com seus mais de 5 Schedulers integrados, o Combine usa mecanismos existentes da Apple: DispatchQueue, RunLoop e OperationQueue. Cada um desses tipos está em conformidade com o protocolo Scheduler, permitindo que sejam passados diretamente para receive(on:) e subscribe(on:) sem adaptadores adicionais.
receive(on:) muda o downstream para o Scheduler especificado — equivalente a observeOn no RxSwift. Todos os operadores após receive(on:) executam no Scheduler especificado. subscribe(on:) muda o upstream — afeta a execução do Publisher. Um padrão típico: subscribe(on: DispatchQueue.global()) para trabalho em segundo plano e receive(on: DispatchQueue.main) para atualizações de UI. No SwiftUI, ao usar .onReceive, a vinculação embutida à thread principal não é necessária, mas para sink, receive(on:).main explícito é recomendado.
// Carregamento em segundo plano + UI na thread 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 é uma alternativa ao DispatchQueue.main para operações de UI. A diferença é que o RunLoop.main está vinculado ao loop de eventos atual do aplicativo, enquanto o DispatchQueue.main está vinculado à fila global da thread principal. Para UIKit, recomenda-se DispatchQueue.main; para SwiftUI — RunLoop.main. ImmediateWhenScheduler executa operações sincronamente na thread atual — usado por padrão para testes e Publishers simples.
ObservableObject é um protocolo do SwiftUI para objetos que publicam mudanças. Uma classe que implementa ObservableObject pode usar o property wrapper @Published para propriedades cujas mudanças notificam automaticamente o SwiftUI sobre a necessidade de redesenhar. Internamente, @Published cria um Publisher que notifica o Publisher objectWillChange quando wrappedValue muda. O SwiftUI assina o objectWillChange através de @StateObject, @ObservedObject ou @EnvironmentObject.
@Published é a forma mais comum de integrar o Combine no SwiftUI. Quando o valor de uma propriedade @Published muda, o SwiftUI atualiza todas as Views que usam esse objeto. @StateObject cria uma instância de ObservableObject e assina suas mudanças. Uma View criada com @StateObject é redesenhada automaticamente quando propriedades @Published mudam. Se o objeto precisar ser compartilhado entre várias Views, @ObservedObject ou @EnvironmentObject são usados.
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 é um wrapper de apagamento de tipo para Cancellable que armazena um token de cancelamento de assinatura. Set<AnyCancellable> gerencia o ciclo de vida das assinaturas: quando o proprietário é desinicializado, todos os Cancellable são cancelados automaticamente. Em projetos SwiftUI, Set<AnyCancellable> é declarado na classe ObservableObject, e as assinaturas são adicionadas através de .store(in: &cancellables). Para UIKit, os mesmos mecanismos são usados com armazenamento em UIViewController através de &cancellables ou chamadas manuais de cancel().
Combine e RxSwift resolvem as mesmas tarefas de programação reativa, mas têm diferenças arquitetônicas fundamentais. O Combine faz parte do SDK da Apple com compatibilidade reversa até o iOS 13, o RxSwift é uma biblioteca de terceiros com suporte ao iOS 8+. O Combine usa tipagem estrita de erros através de Failure genérico, o RxSwift usa um único tipo Error. O Combine é integrado ao SwiftUI no nível da plataforma, o RxSwift requer RxCocoa para extensões de UI.
A escolha entre Combine e RxSwift depende dos requisitos do projeto. Se a versão mínima do iOS for >= 13 e o projeto usar SwiftUI — o Combine é a escolha natural graças à integração embutida e à ausência de dependências adicionais. Se o projeto suporta iOS 11-12, contém uma base de código RxSwift existente ou requer operadores específicos disponíveis apenas no RxSwift (por exemplo, Observable.from(path:)), — o RxSwift continua sendo uma solução válida.
| Característica | Combine | RxSwift |
|---|---|---|
| Desenvolvedor | Apple (integrado ao SDK) | ReactiveX (comunidade) |
| Versão iOS | iOS 13+ | iOS 8+ |
| Tipo de Erro | Failure genérico (Never para UI) | Error (qualquer) |
| Integração UI | @Published + SwiftUI | RxCocoa + UIKit |
| Operadores | ~100 integrados | 400+ operadores |
| Swift Concurrency | Através de .values (sequência async) | Através de biblioteca ponte |
Perguntas Frequentes
PassthroughSubject não armazena estado — um assinante recebe apenas eventos enviados após a assinatura. CurrentValueSubject armazena o valor atual e o passa para cada novo assinante imediatamente ao assinar. CurrentValueSubject é adequado para representar estado (por exemplo, isLoggedIn).
Uma assinatura retorna AnyCancellable, que é cancelado ao chamar cancel() ou na desinicialização. Para gerenciamento em grupo, use Set<AnyCancellable> — todas as assinaturas são canceladas quando o conjunto é limpo. Isso é análogo ao DisposeBag no RxSwift.
Sim, o Combine continua relevante para dados em streaming: eventos de UI, debounce, combineLatest, WebSocket. async/await é conveniente para requisições únicas, Combine — para fluxos contínuos ou múltiplos. Ambos os frameworks são complementares — Publisher pode ser convertido em AsyncSequence.
UIKit não tem Publishers integrados, mas a Apple fornece extensões: NotificationCenter.default.publisher(for:), Timer.publish, URLSession.dataTaskPublisher. Para eventos de UI personalizados, PassthroughSubject ou @IBAction encapsulados em um Publisher através de Future ou Subject são usados.
Backpressure é um mecanismo para controlar a velocidade do fluxo: o Subscriber informa ao Publisher através de Demand quantos elementos está pronto para processar. Se Demand = .max(1), o Publisher aguarda uma solicitação antes de enviar o próximo valor. Isso evita estouro de buffer quando as velocidades do produtor e consumidor não coincidem.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também