Engenharia iOS

Por que usar RxSwift e Combine no iOS? Problemas resolvidos pela programação reativa

Ao estudar desenvolvimento iOS, em algum momento você inevitavelmente encontra RxSwift ou Combine. Eles aparecem em quase toda vaga, mas abrir o código pode ser desconcertante: uma sintaxe desconhecida encadeia map, flatMap e sink. “Se closures e delegates já funcionam, eu realmente preciso aprender isso?”

6 min de leitura
Imagem de capa de Por que usar RxSwift e Combine no iOS? Problemas resolvidos pela programação reativa

Ao estudar desenvolvimento iOS, em algum momento você inevitavelmente encontra RxSwift ou Combine. Eles aparecem em quase toda vaga, mas abrir o código pode ser desconcertante quando map, flatMap e sink aparecem em sequência. “Se closures e delegates já funcionam, eu realmente preciso aprender isso?” É natural ter essa dúvida.

Hoje vamos começar, antes da sintaxe, por por que essas ferramentas se tornaram necessárias. Quando você entende o motivo, a sintaxe vem naturalmente.


Apps iOS são, na verdade, “máquinas de processar eventos”

Ao decompor o que um app faz, a maior parte é responder a eventos.

  • O usuário toca em um botão → muda a tela
  • Chega uma resposta de rede → atualiza a lista
  • O teclado aparece → move o campo de entrada para cima
  • O valor do campo de texto muda → solicita os resultados da busca novamente

O problema é que o UIKit entrega esses eventos de maneiras diferentes.

Fonte do evento Forma de entrega
Toque no botão target-action
Rolagem da table view delegate
Resposta de rede completion handler (closure)
Aparição do teclado NotificationCenter
Alteração de propriedade do objeto KVO(Key-Value Observing)

No iOS, é perfeitamente comum usar essas cinco formas na mesma tela. A tarefa é a mesma — reagir a um evento —, mas o código fica espalhado em cinco formatos. Para entender o comportamento de uma tela, você precisa procurar por todo o view controller.

Esse é o primeiro problema que RxSwift e Combine tentam resolver. Eles unificam todos os eventos como um “fluxo de valores ao longo do tempo”. Toques, respostas de rede e notificações do teclado passam a usar a mesma interface.


Duas barreiras encontradas ao sobreviver com callbacks

Barreira 1: Composição de tarefas assíncronas

Um requisito comum é: “Solicitar ao mesmo tempo os dados do usuário e as publicações recentes e desenhar a tela quando ambos chegarem”. Com completion handlers, fica assim.

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 { /* E onde fica o tratamento de erros?? */ return }
        render(user, posts)
    }
}

É preciso usar DispatchGroup, variáveis temporárias e tratamento de erros espalhado. Quando as solicitações chegam a três ou quatro, e entram dependências como “quando A terminar, solicite B com o resultado”, o aninhamento sai de controle. É o inferno dos callbacks.

No Combine, o mesmo requisito é expresso assim.

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)

“Agrupe os dois com zip, receba na thread principal e notifique em caso de sucesso ou falha.” O código praticamente reproduz o requisito. O tratamento de erros também fica centralizado.

Barreira 2: Controle de eventos contínuos

Pense em um campo de busca. Se você chamar a API a cada tecla, digitar “Swift” envia cinco solicitações. Por isso, normalmente são adicionadas condições como estas.

  • Solicitar somente 0,3 segundo depois que a entrada parar (debounce)
  • Não solicitar se for igual à busca anterior (remoção de duplicatas)
  • Quando uma nova solicitação começar, cancelar a anterior se ela ainda não terminou

Implementar isso diretamente com Timer e variáveis de flag cria um código propenso a bugs, pois a invalidação do timer e o cancelamento da solicitação ficam interligados. No Combine, basta encadear operadores comprovados.

searchTextSubject
    .debounce(for: .seconds(0.3), scheduler: DispatchQueue.main)
    .removeDuplicates()
    .map { api.search(query: $0) }
    .switchToLatest()   // Cancelar automaticamente a solicitação anterior quando chegar uma nova
    .sink { results in render(results) }
    .store(in: &cancellables)

O ponto central é este: montar componentes comprovados em vez de implementar diretamente a lógica de controle de eventos baseada em tempo. É onde o valor da programação reativa fica mais evidente.

Monte operadores comprovados em vez de implementar o controle de tempo diretamente
Monte operadores comprovados em vez de implementar o controle de tempo diretamente

Componentes padrão do binding de MVVM

Como vimos no artigo anterior, MVVM (Model-View-ViewModel) só fica completo quando há binding para que a View acompanhe automaticamente as mudanças do ViewModel. O UIKit não possui binding integrado. Quem preencheu essa lacuna foi o RxSwift (RxCocoa), e desde o iOS 13 o Combine assumiu esse papel.

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

Essa é a razão prática pela qual tantas vagas exigem RxSwift ou Combine. Em codebases baseadas em MVVM, a camada de binding é, na prática, construída com um desses dois frameworks.


RxSwift e Combine: qual é a diferença?

Conceitualmente, os dois são ferramentas de programação reativa. Não é exagero dizer que Observable virou Publisher e subscribe virou sink, mudando basicamente os nomes. Aprendendo um, você consegue migrar para o outro com uma tabela de correspondência. Ainda assim, os critérios de escolha são claros.

  • RxSwift: biblioteca de terceiros. Não há restrição de versão do iOS, o RxCocoa oferece binding rico para UIKit, e documentação acumulada e comunidade são pontos fortes. Em contrapartida, há uma dependência externa e o tempo de build aumenta.
  • Combine: framework first-party da Apple. Pode ser usado no iOS 13 ou posterior sem adicionar dependências e se integra naturalmente aos @Published e ObservableObject do SwiftUI. Porém, o suporte a binding de UIKit é mais limitado que o do RxCocoa.

Para um projeto novo, Combine é o padrão mais seguro. RxSwift costuma ser aprendido ao entrar em uma equipe cujo código existente já o utiliza.

RxSwift e Combine são duas caixas de ferramentas para o mesmo trabalho; a situação da equipe define a escolha
RxSwift e Combine são duas caixas de ferramentas para o mesmo trabalho; a situação da equipe define a escolha

Ainda é necessário depois que async/await surgiu?

Desde o async/await do Swift 5.5, é comum perguntar: “Então não preciso mais aprender Combine?”. Isso é parcialmente verdade. Para uma operação assíncrona pontual que termina com “uma solicitação, uma resposta”, async/await é muito mais legível. O exemplo de perfil acima pode ser feito em async let apenas duas linhas.

Mas fluxos de valores que continuam chegando, como o campo de busca, são diferentes. Quando eventos contínuos —entrada de texto, atualizações de localização, mensagens de WebSocket ou mudanças de estado do ViewModel— exigem controles baseados em tempo, como debounce ou combineLatest, as ferramentas reativas ainda são apropriadas. A Apple também amplia essa área com AsyncSequence, mas o ecossistema de operadores ainda é mais completo no Combine.

Em resumo:

  • Assincronia pontual (como uma solicitação de rede) → async/await
  • Fluxos contínuos de eventos + controle de tempo e binding de UI → Combine (ou RxSwift)

Eles não são tanto concorrentes quanto ferramentas com áreas de responsabilidade diferentes.


Conclusão

Em uma frase, usamos RxSwift e Combine para unificar eventos assíncronos distintos em uma interface de fluxo e tratar composição, controle de tempo e binding de forma declarativa sobre ela.

  • Unifique em uma única abordagem o tratamento de eventos espalhado entre delegates, closures e notificações
  • Expresse a composição de várias tarefas assíncronas sem callbacks aninhados
  • Resolva com operadores comprovados controles de tempo como debounce, remoção de duplicatas e cancelamento de solicitações
  • Componente padrão de fato do binding de MVVM

A sintaxe parece intimidante, mas, ao entender a ideia de “ver eventos como fluxos de valores”, os operadores não passam de map e filter de arrays estendidos no eixo do tempo. No próximo artigo, vamos dissecar como Publisher e Subscriber do Combine se conectam e funcionam internamente.

Leitura recomendada