A programação reativa tornou-se uma abordagem fundamental no desenvolvimento de aplicações modernas, especialmente ao lidar com fluxos de dados assíncronos e interfaces dinâmicas. Em 2024, seu uso torna-se cada vez mais relevante tanto para a criação de projetos do zero quanto para a manutenção de sistemas legados que já utilizam frameworks como RxSwift ou Combine .
Neste artigo, exploraremos o que é programação reativa, por que ela é importante e como você pode implementá-la em seus projetos UIKit . Além disso, apresentaremos um exemplo prático comparando abordagens tradicionais com programação reativa usando RxSwift e Combine , medindo seu desempenho e eficiência em tempo real.
O que é Programação Reativa?
A programação reativa permite que seu aplicativo reaja a fluxos de dados e eventos de forma declarativa. Em vez de verificar constantemente o estado de diferentes partes da sua interface ou dados, o sistema gerencia automaticamente as atualizações, reagindo a eventos de maneira eficiente e previsível.
Em UIKit, isso pode ser extremamente útil ao lidar com tarefas como atualizações de interface do usuário, chamadas de rede ou interações do usuário, especialmente quando estas são assíncronas.
Por que a programação reativa é importante?
- Gerenciamento de eventos assíncronos : Muitas operações em aplicativos são assíncronas (solicitações de rede, animações, cliques em botões), e a programação reativa torna o gerenciamento desses fluxos de dados mais organizado.
- Escalabilidade : Em projetos de grande porte, sem programação reativa, o código que lida com eventos e estados pode se tornar complexo, com muitas funções de retorno de chamada (callbacks) e closures. A programação reativa abstrai essa complexidade e oferece uma maneira mais limpa de gerenciá-la.
- Desempenho aprimorado : Quando combinadas com o UIKit, estruturas como RxSwift e Combine garantem que eventos e alterações na interface do usuário sejam propagados de forma eficiente, otimizando o desempenho do aplicativo.
Conceitos-chave para estudar em Programação Reativa com UIKit
Antes de abordarmos exemplos práticos, é fundamental compreender alguns conceitos-chave:
- Vinculação : Como conectar fontes de dados reativas à interface do usuário.
- Observáveis vs. Publicadores : Entendendo os observáveis no RxSwift e os publicadores no Combine.
- Operadores reativos : Operadores como `map`
map, ` filter` e `react`filter,flatMape como eles transformam e combinam fluxos de dados. - Gerenciamento do ciclo de vida : Utilizando
DisposeBag(RxSwift) eAnyCancellable(Combine) para gerenciar a memória e evitar vazamentos. - Observação de eventos da interface do usuário : como conectar controles do UIKit, como botões, tabelas e campos de texto, a fluxos de eventos reativos.
Exemplo prático: Atualização da interface do usuário com chamada de API
Vamos implementar uma funcionalidade de busca simples que recupera dados do usuário de uma API usando três abordagens diferentes: callbacks tradicionais, RxSwift e Combine. Isso nos permite observar as diferenças em termos de desempenho e simplicidade.
Abordagem não reativa (UIKit usando código de visualização)
A seguir, apresentamos uma implementação tradicional onde gerenciamos manualmente os eventos de entrada do usuário e as respostas da rede usando callbacks.
import UIKit
class ViewController : UIViewController {
private let searchTextField: UITextField = {
let textField = UITextField ()
textField.placeholder = "Pesquisar usuários"
textField.borderStyle = .roundedRect
return textField
}()
private let tableView: UITableView = {
let tableView = UITableView ()
return tableView
}()
var data = [ String ]() // Dados de resposta da API simulados
override func viewDidLoad () {
super .viewDidLoad()
setupViews()
searchTextField.addTarget( self , action: #selector (textFieldDidChange( _ :)), for: .editingChanged)
}
private func setupViews () {
view.addSubview(searchTextField)
view.addSubview(tableView)
searchTextField.translatesAutoresizingMaskIntoConstraints = false
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate ([
searchTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16 ),
searchTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16 ),
searchTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: - 16 ),
tableView.topAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 16 ),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
@objc func textFieldDidChange ( _ textField : UITextField ) {
guard let query = textField.text, ! query.isEmptyelse { return }
searchUsers(query: query) { [ weak self ] users in
self ? .data = users
self ? .tableView.reloadData()
}
}
func searchUsers ( query : String , completion : @escaping ([ String ]) -> Void ) {
// Chamada de API simulada
DispatchQueue .global().asyncAfter(deadline: .now() + 1 ) {
let results = [ "User1" , "User2" , "User3" ] // Resposta simulada
completion(results)
}
}
}
Problemas com essa abordagem:
- Você gerencia manualmente os eventos e as atualizações da interface do usuário.
- As funções de retorno de chamada (callbacks) dificultam a escalabilidade e a manutenção do código.
- À medida que a complexidade aumenta, o código torna-se mais propenso a erros e mais difícil de depurar.
Solução com RxSwift (UIKit usando View Code)
Agora, vamos reescrever o mesmo exemplo usando RxSwift para aproveitar a programação reativa.
import RxSwift
import RxCocoa
class ViewController : UIViewController {
private let searchTextField: UITextField = {
let textField = UITextField ()
textField.placeholder = "Pesquisar usuários"
textField.borderStyle = .roundedRect
return textField
}()
private let tableView: UITableView = {
let tableView = UITableView ()
return tableView
}()
let disposeBag = DisposeBag ()
var data = [ String ]()
override func viewDidLoad () {
super .viewDidLoad()
setupViews()
// Programação reativa para observar a entrada do campo de texto
searchTextField.rx.text.orEmpty
.debounce(.milliseconds( 300 ), scheduler: MainScheduler .instance)
.distinctUntilChanged()
.flatMapLatest { query in
self .searchUsers(query: query)
}
.observe(on: MainScheduler.instance )
.subscribe(onNext: { [ weak self ] users in
self ? .data = users
self ? .tableView.reloadData()
})
.disposed(by: disposeBag)
}
private func setupViews () {
view.addSubview(searchTextField)
view.addSubview(tableView)
searchTextField.translatesAutoresizingMaskIntoConstraints = false
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate ([
searchTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16 ),
searchTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16 ),
searchTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16 ),
tableView.topAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 16 ),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
func searchUsers ( query : String ) -> Observable <[ String ]> {
return Observable .create { observer in
DispatchQueue .global().asyncAfter(deadline: .now() + 1 ) {
let results = [ "User1" , "User2" , "User3" ]
observer.onNext(results)
observer.onCompleted()
}
return Disposables .create()
}
}
}
Melhorias com o RxSwift:
- O fluxo de eventos é muito mais claro e organizado.
- Menos código manual e menos funções de retorno de chamada aninhadas.
- Operadores como esses
debounceajudam a otimizar o desempenho, evitando buscas repetidas enquanto o usuário digita.
Solução com Combine (UIKit usando View Code)
Agora vamos implementar a mesma funcionalidade usando o Combine.
import Combine
class ViewController : UIViewController {
private let searchTextField: UITextField = {
let textField = UITextField ()
textField.placeholder = "Pesquisar usuários"
textField.borderStyle = .roundedRect
return textField
}()
private let tableView: UITableView = {
let tableView = UITableView ()
return tableView
}()
var data = [ String ]()
var cancellables = Set < AnyCancellable >()
override func viewDidLoad () {
super .viewDidLoad()
setupViews()
NotificationCenter .default.publisher(for: UITextField .textDidChangeNotification, object: searchTextField)
.compactMap { ( $0 .object as? UITextField ) ? .text }
.debounce(for: .milliseconds( 300 ), scheduler: RunLoop .main)
.removeDuplicates()
.flatMap { query in
self .searchUsers(query: query)
}
.receive(on: DispatchQueue .main)
.sink { [ weak self ] users in
self ? .data = users
self ? .tableView.reloadData()
}
.store(in: & cancellables)
}
private func setupViews () {
view.addSubview(searchTextField)
view.addSubview(tableView)
searchTextField.translatesAutoresizingMaskIntoConstraints = false
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint .activate([
searchTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16 ),
searchTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16 ),
searchTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: - 16 ),
tableView.topAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 16 ),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
func searchUsers ( query : String ) -> AnyPublisher <[ String ], Never > {
return Future <[ String ], Never > { promise in
DispatchQueue .global().asyncAfter(deadline: .now() + 1 ) {
let results = [ "User1" , "User2" , "User3" ]
promise(.success(results))
}
}
.eraseToAnyPublisher()
}
}
Melhorias com o Combine:
- Sintaxe simplificada, especialmente com a integração nativa do UIKit através de
NotificationCenter. - A gestão do ciclo de vida para editores é feita de forma eficiente com
store(in:). - Excelente integração com os recursos modernos do Swift, tornando-o ideal para novos projetos.
Quando usar programação reativa no UIKit
- Novos projetos : Se o seu projeto é novo e você deseja construir algo escalável e fácil de manter, tanto o RxSwift quanto o Combine são ótimas opções para gerenciar eventos assíncronos.
- Projetos Legados : Para aplicativos UIKit existentes, é essencial avaliar se sua equipe já está familiarizada com o RxSwift. Nesse caso, o RxSwift pode ser a escolha mais segura, mas o Combine oferece uma abordagem mais moderna e integrada, especialmente se você planeja migrar para o SwiftUI no futuro.
Projeto: Comparando as abordagens tradicional, RxSwift e Combine no UIKit
Introdução
Neste projeto, implementaremos três versões diferentes de uma funcionalidade de busca que obtém dados de uma API fictícia. Usaremos três abordagens de programação diferentes em paralelo para compará-las:
- Sem Programação Reativa (callbacks tradicionais)
- Com RxSwift (programação reativa com RxSwift)
- Com Combine (programação reativa com Combine)
Também configuraremos registros para medir o tempo de execução e o desempenho de cada abordagem em tempo real.
1. Configurando o projeto no Xcode
- Abra o Xcode e crie um novo aplicativo de visualização única .
- Dê o nome de ReactiveProgrammingComparison ao projeto .
- Escolha Swift como linguagem e UIKit como interface.
- Certifique-se de que as opções “Usar dados principais” e “Incluir testes” estejam desmarcadas.
2. Instalando as dependências do RxSwift
- RxSwift : Utilize o CocoaPods ou o Swift Package Manager para instalar o RxSwift no projeto.
- Usando o CocoaPods , adicione o seguinte ao seu arquivo
Podfile:
pod 'RxSwift' , '6.0'
pod 'RxCocoa' , '6.0'
- Utilizando o Swift Package Manager :
- No Xcode, acesse Arquivo > Pacotes Swift > Adicionar Dependência de Pacote .
- Adicione o URL:
https://github.com/ReactiveX/RxSwift.git.
- Combine é nativo do Swift, portanto, nenhuma instalação é necessária.
3. Implementando o layout da interface do usuário com UIKit
Criaremos um layout simples dividido em três seções. Cada seção exibirá o resultado da pesquisa de acordo com a abordagem de programação utilizada.
import UIKit
class ViewController : UIViewController {
// Rótulos para exibir resultados de pesquisa e tempos de resposta
private let traditionalLabel: UILabel = {
let label = UILabel ()
label.text = "Tradicional:"
label.font = UIFont .systemFont(ofSize: 18 )
return label
}()
private let rxSwiftLabel: UILabel = {
let label = UILabel ()
label.text = "RxSwift:"
label.font = UIFont .systemFont(ofSize: 18 )
return label
}()
private let combineLabel: UILabel = {
let label = UILabel ()
label.text = "Combinar:"
label.font = UIFont .systemFont(ofSize: 18 )
return label
}()
// Botão para acionar a busca de dados
private let fetchButton: UIButton = {
let button = UIButton (type: .system)
button.setTitle( "Buscar Dados" , for: .normal)
button.addTarget( self , action: #selector (fetchData), for: .touchUpInside)
return button
}()
override func viewDidLoad () {
super .viewDidLoad()
setupViews()
}
private func setupViews () {
view.addSubview(traditionalLabel)
view.addSubview(rxSwiftLabel)
view.addSubview(combineLabel)
view.addSubview(fetchButton)
traditionalLabel.translatesAutoresizingMaskIntoConstraints = false
rxSwiftLabel.translatesAutoresizingMaskIntoConstraints = false
combineLabel.translatesAutoresizingMaskIntoConstraints = false
fetchButton.translatesAutoresizingMaskIntoConstraints= false
NSLayoutConstraint.activate ([
traditionalLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20 ),
traditionalLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
rxSwiftLabel.topAnchor.constraint(equalTo: traditionalLabel.bottomAnchor, constant: 20 ),
rxSwiftLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
combineLabel.topAnchor.constraint(equalTo: rxSwiftLabel.bottomAnchor, constant: 20 ),
combineLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
fetchButton.topAnchor.constraint(equalTo: combineLabel.bottomAnchor, constant: 40 ),
fetchButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
@objc private func fetchData () {
// Acionar todas as 3 abordagens simultaneamente
fetchDataTraditionally()
fetchDataRxSwift()
fetchDataCombine()
}
}
4. Implementando a abordagem tradicional de retorno de chamada
func fetchDataTraditionally () {
let startTime = Date ()
// Simulando uma chamada de API com atraso
DispatchQueue.global ().asyncAfter(deadline: .now() + 1.5 ) {
let fetchedData = [ "User1" , "User2" , "User3" ]
DispatchQueue.main.async {
let timeElapsed = Date ().timeIntervalSince(startTime)
self.traditionalLabel.text = "Tradicional: \(fetchedData) ( \(timeElapsed) segundos) "
}
}
}
Nessa abordagem tradicional, gerenciamos manualmente os eventos e medimos o tempo de execução usando Date().
5. Implementando a abordagem RxSwift
import RxSwift
func fetchDataRxSwift () {
let startTime = Date ()
let disposeBag = DisposeBag ()
Observable .just([ "User1" , "User2" , "User3" ])
.delay(.seconds( 2 ), scheduler: MainScheduler .instance)
.subscribe(onNext: { [ weak self ] fetchedData in
let timeElapsed = Date ().timeIntervalSince(startTime)
self ? .rxSwiftLabel.text = "RxSwift: \(fetchedData) ( \(timeElapsed) seg)"
})
.disposed(by: disposeBag)
}
Na implementação do RxSwift, simulamos a resposta da API Observable.juste a atrasamos em 2 segundos. O tempo de execução é então exibido.
6. Implementando a Abordagem Combinada
import Combine
func fetchDataCombine () {
let startTime = Date ()
var cancellables = Set <AnyCancellable> ( )
Just ([ "User1" , "User2" , "User3" ])
.delay(for: .seconds( 1.8 ), scheduler: DispatchQueue.main )
.sink(receiveCompletion: { _ in }, receiveValue: { [ weak self ] fetchedData in
let timeElapsed = Date ().timeIntervalSince(startTime)
self ? .combineLabel.text = "Combine: \(fetchedData) ( \(timeElapsed) seg)"
})
.store(in: & cancellables)
}
Aqui, usamos Justo publisher do Combine para simular a resposta e delayintroduzir um atraso de 1,8 segundos.
7. Medição de desempenho em tempo real
Com o projeto agora em andamento, podemos medir o desempenho de cada abordagem usando registros e o Instruments .
Desempenho de registro
Adicionamos uma função de registro simples para monitorar o uso da CPU em tempo real:
func logPerformance ( method : String ) {
let usage = ProcessInfo .processInfo.systemUptime
print ( " \(method) - Uso da CPU: \(usage) " )
}
Adicione este registro ao final de cada método fetch:
self ? .logPerformance(método: "Traditional" )
self ? .logPerformance(método: "RxSwift" )
self ? .logPerformance(método: "Combine" )
Utilizando instrumentos
- Abra o Instruments no Xcode (
Command + Ienquanto o aplicativo estiver em execução). - Selecione o Time Profiler para medir o uso da CPU, o consumo de memória e o tempo de execução.
- Clique em “Obter dados” no simulador e observe as diferenças entre as três abordagens.
Tabela de comparação de desempenho
Agora que implementamos as três abordagens, vamos resumir as diferenças de desempenho:

Quando não usar programação reativa
Embora a programação reativa ofereça inúmeras vantagens, nem sempre é a melhor escolha. Aqui estão alguns cenários em que ela pode não ser viável:
- Aplicações simples : Para aplicativos com poucas telas e interações limitadas, adicionar uma camada de programação reativa pode ser um exagero. O custo de aprendizado e a complexidade adicional podem não justificar seu uso.
- Curva de Aprendizagem : Para equipes que são novas na programação reativa, o tempo necessário para aprender conceitos como observables, publishers e operators pode atrasar o desenvolvimento, especialmente se os benefícios não forem claros para as necessidades do projeto.
- Operações síncronas e simples : Se seu aplicativo lida principalmente com operações síncronas, como toques em botões ou fluxos de navegação simples, a complexidade adicional da programação reativa pode não trazer benefícios significativos.
Qual abordagem você deve escolher?
A escolha entre RxSwift e Combine depende das necessidades do seu projeto e dos requisitos da plataforma. O Combine é a melhor opção para novos projetos que visam o ecossistema da Apple e se beneficiam de sua integração nativa. O RxSwift continua sendo uma opção sólida para projetos legados ou aqueles que exigem suporte multiplataforma.
Aqui está um breve resumo:
- Combine : Ideal para projetos novos, focados em SwiftUI e direcionados a plataformas Apple.
- RxSwift : Ideal para projetos legados ou multiplataforma com implementações Rx existentes.
- Tradicional (Não Reativo) : Adequado para aplicativos pequenos e simples com complexidade mínima.
A comparação em tempo real neste projeto mostrou o desempenho de cada abordagem em termos de tempo de execução e consumo de recursos. Testar esses métodos em seus próprios projetos lhe dará uma melhor noção de qual deles se adapta melhor às suas necessidades.
Agora que você entende a programação reativa, tente implementá-la em seu projeto atual ou futuro e veja a diferença que ela faz!
Boa programação!




