Mobile

30 mar, 2026

Programação Reativa em UIKit | Guia Completo

Publicidade

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?

  1. 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.
  2. 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.
  3. 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:

  1. Vinculação : Como conectar fontes de dados reativas à interface do usuário.
  2. Observáveis ​​vs. Publicadores : Entendendo os observáveis ​​no RxSwift e os publicadores no Combine.
  3. Operadores reativos : Operadores como `map` map, ` filter` e `react` filterflatMape como eles transformam e combinam fluxos de dados.
  4. Gerenciamento do ciclo de vida : Utilizando DisposeBag(RxSwift) e AnyCancellable(Combine) para gerenciar a memória e evitar vazamentos.
  5. 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:

  1. Sem Programação Reativa (callbacks tradicionais)
  2. Com RxSwift (programação reativa com RxSwift)
  3. 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

  1. Abra o Xcode e crie um novo aplicativo de visualização única .
  2. Dê o nome de ReactiveProgrammingComparison ao projeto .
  3. Escolha Swift como linguagem e UIKit como interface.
  4. 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 :
  1. No Xcode, acesse Arquivo > Pacotes Swift > Adicionar Dependência de Pacote .
  2. 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

  1. Abra o Instruments no Xcode ( Command + Ienquanto o aplicativo estiver em execução).
  2. Selecione o Time Profiler para medir o uso da CPU, o consumo de memória e o tempo de execução.
  3. 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:

Pressione Enter ou clique para ver a imagem em tamanho real.

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:

  1. 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.
  2. 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.
  3. 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!