Introdução à Programação Reativa com Combine no iOS

As aplicações iOS modernas são inerentemente assíncronas. Desde as solicitações de rede e consultas de banco de dados até eventos de interface de usuário e dados de sensores, os desenvolvedores devem gerenciar um fluxo constante de informações que chegam em momentos imprevisíveis. Tradicionalmente, lidar com esses eventos são necessários delegados, callbacks ou centros de notificação, o que muitas vezes levou a um código complexo e propensa a erros. A estrutura Combine, introduzida pela Apple no iOS 13, oferece uma abordagem unificada e declarativa para programação reativa que simplifica o manuseio de eventos assíncronos e fluxos de dados. Ao usar editores, assinantes e operadores, a Combine permite escrever pipelines limpos e composíveis que reagem às mudanças de forma eficiente.

Este artigo fornece um guia prático completo para usar a Combine para programação reativa no iOS, desde padrões fundamentais até padrões avançados.

O que é o Combine Framework?

Combine é uma estrutura nativa Swift que fornece uma API declarativa para o processamento de valores ao longo do tempo. Ele faz parte do ecossistema da Apple e se integra perfeitamente com SwiftUI, Foundation e UIKit. Combine é construído sobre três abstrações de núcleo: ]publicadores emitem valores, assinantes[ recebe e processa esses valores, e operadores[ transformam ou filtram o fluxo de valores. Juntos, eles criam um pipeline que define como os fluxos de dados através de sua aplicação.

Ao contrário de padrões anteriores, como ou KVO, a Combine oferece segurança de tempo de compilação, inferência de tipo e composição poderosa. É similar em conceito a extensões reativas como o RxSwift, mas é de primeira e profundamente integrada com as plataformas da Apple. Entender Combine é essencial para desenvolvedores iOS modernos, especialmente aqueles que constroem aplicativos com SwiftUI, onde Combine pode ter muitos recursos orientados a dados.

Conceitos Principais de Combinar

Antes de mergulhar em código, é fundamental captar os quatro componentes primários que formam cada pipeline Combine:

  • Publisher – Um tipo que pode emitir uma sequência de valores ao longo do tempo. Declara que tipo de valores ele produz ()]) e que tipo de erros pode falhar com (). Um editor pode emitir um número arbitrário de valores, então eventualmente completar ou falhar.
  • Subscritor – Um tipo que recebe valores de um editor. Ele define três métodos: , (para lidar com um novo valor), e (para lidar com sucesso ou falha). A maioria dos desenvolvedores usa assinantes prontos como ou em vez de implementar um assinante personalizado.
  • Operador – Um editor que transforma valores de um editor a montante. Operadores como , , , e são eles mesmos editores que se inscrevem em editores a montante, aplicam uma transformação e republicam o resultado. Este encadeamento é o que torna a Combine pipelines poderosa.
  • Assinatura – A conexão entre um editor e um assinante. Uma assinatura controla o fluxo de demanda (repressão).As assinaturas podem ser canceladas para parar de receber valores, que é tratada automaticamente quando um é desocupado ou manualmente cancelado.

Adicionalmente, o Combine fornece Protocolo cancelável (e o tipo de concreto ]) para gerenciar o ciclo de vida de uma assinatura. Armazenar cancellables em um conjunto (muitas vezes ]) garante que as assinaturas são automaticamente demolidas quando o objeto proprietário é desfeito.

Criando Editores

A Combine fornece muitos editores integrados que cobrem tarefas assíncronas comuns. Você também pode criar editores personalizados implementando o protocolo ou usando inicializadores de conveniência.

Editor de temporizadores

Use para emitir a data atual em intervalos regulares. Para iniciar o temporizador imediatamente, ligue para :

let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()

O editor emite valores a cada segundo no loop principal de execução.

Editor de Tarefas de Dados de Sessão em URLSession

Combine estende com um editor que executa solicitações de rede:

guard let url = URL(string: "https://api.example.com/data") else { return }
let publisher = URLSession.shared.dataTaskPublisher(for: url)

Este editor emite uma tupla após a conclusão, ou um sobre o fracasso.

Editor de NotificaçõesCenter

Converta qualquer notificação em um editor:

let notificationPublisher = NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)

Útil para reagir a eventos do sistema sem adicionar observadores manualmente.

Justo e Futuro

emite um único valor e então completa imediatamente. É perfeito para testar ou envolver uma constante em um editor:

let justPublisher = Just("Hello, Combine!")

é um editor que produz um único valor (ou falha) assincronicamente. Ele executa o fechamento fornecido exatamente uma vez e armazena o resultado para assinantes posteriores:

let futurePublisher = Future<String, Error> { promise in
 // Perform async work, then call promise(.success(value)) or promise(.failure(error))
}

Subjeto de passagem e Valor de CorrenteSubjeto

Os sujeitos atuam como um editor e um assinante, permitindo que você injete manualmente valores em um fluxo. transmite valores para assinantes sem manter um valor atual; mantém o valor mais recente e reproduz-o para novos assinantes:

let subject = PassthroughSubject<Int, Never>()
subject.send(1)
subject.send(2)

let currentValueSubject = CurrentValueSubject<String, Never>("initial")
currentValueSubject.send("updated")
print(currentValueSubject.value) // "updated"

Subscrição a um Editor

Para ativar um gasoduto, você precisa de um assinante. Os mais comumente usados são e .

O Assinante do Sink

fornece tratamento baseado no encerramento para os valores recebidos e eventos de conclusão:

let subscription = timerPublisher.sink { time in
 print("Timer fired at \(time)")
}

Você também pode lidar com erros, fornecendo um encerramento:

publisher
 .sink(receiveCompletion: { completion in
 switch completion {
 case .finished:
 print("Stream finished")
 case .failure(let error):
 print("Error: \(error)")
 }
 }, receiveValue: { value in
 print("Received: \(value)")
 })

O Assinante de Atribuir

] liga o resultado do editor a uma propriedade de um objeto, opcionalmente através de um caminho chave. A propriedade deve ser marcada com ou ser compatível com o KVO:

class ViewModel {
 @Published var text: String = ""
}

let viewModel = ViewModel()
let publisher = Just("Hello")
let cancellable = publisher.assign(to: \.text, on: viewModel)
print(viewModel.text) // "Hello"

Assinante Personalizado

Para casos de uso avançado, você pode implementar o protocolo diretamente. No entanto, isso raramente é necessário; ] e cobrem a maioria dos cenários.

Transformando dados com operadores

Os operadores são a espinha dorsal da Combine, permitindo que você componha oleodutos complexos de blocos de construção simples. Aqui estão os mais essenciais:

Mapa

Transforma cada valor emitido usando um fechamento:

publisher
 .map { $0 * 2 }
 .sink { print($0) }

Filtro

Passa por valores que satisfazem um predicado:

publisher
 .filter { $0.isMultiple(of: 2) }
 .sink { print("Even: \($0)") }

FlatMap

Aplana vários editores internos para um único fluxo. Comumente usado quando cada valor desencadeia uma nova operação assíncrona:

let searchPublisher = searchTextPublisher
 .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
 .removeDuplicates()
 .flatMap { text in
 return api.searchPublisher(for: text)
 }

CombinarÚltimo

Combina os últimos valores de dois ou mais editores em uma tupla. Emite um novo valor sempre que qualquer um dos editores emite:

Publishers.CombineLatest(usernamePublisher, passwordPublisher)
 .map { username, password in
 return username.count > 0 && password.count > 0
 }
 .assign(to: \.isEnabled, on: loginButton)

Zip

Emparelha valores de vários editores em ordem, esperando que todos os editores emitem o próximo valor antes de emitir o par:

Publishers.Zip(emailsPublisher, namesPublisher)
 .sink { email, name in
 print("\(name): \(email)")
 }

Juntar

Intermete valores de vários editores do mesmo tipo em um único fluxo:

Publishers.Merge(leftPublisher, rightPublisher)
 .sink { print($0) }

Coletar

Buffers valores em arrays, emitindo uma vez que o buffer enche ou o fluxo completa:

publisher
 .collect(3) // emit arrays of 3 values
 .sink { print($0) }

Denunciar e Acelerar

espera por uma pausa especificada entre os valores antes de emitir o último. emite no máximo um valor por intervalo de tempo. Ambos são essenciais para o tratamento de eventos de alta frequência, como entrada de texto:

textField.textPublisher
 .debounce(for: .seconds(0.3), scheduler: RunLoop.main)
 .sink { text in
 // Perform search after user stops typing
 }

Erro ao lidar com a combinação

Combinar pipelines pode falhar. O tipo ] de um editor define quais erros ele pode emitir. Quando um editor falha, a assinatura é terminada. Use operadores de gerenciamento de erros para recuperar ou transformar erros.

tryMap e tryFilter

Se o seu fechamento de transformação pode lançar, use ou . O gasoduto falha se um erro for lançado:

publisher
 .tryMap { value in
 guard value > 0 else { throw InputError.invalid }
 return value * 2
 }

Captura

Substituir uma falha por um editor alternativo. Útil para fornecer valores de recuo:

publisher
 .catch { error in
 return Just(0) // fallback value
 }

Repetir

Automaticamente, reassina ao editor a montante um número especificado de vezes, se falhar:

publisher
 .retry(3)
 .sink(receiveCompletion: { ... }, receiveValue: { ... })

substituirErro e substituiçãoEmpty

substitui uma falha por um dado valor. fornece um valor se o editor completa sem emitir nada.

Gestão de Memórias e Cancelamento

Cada assinatura consome recursos e deve ser cancelada corretamente para evitar vazamentos de memória. Combine fornece , uma referência cancelável com apagável com apagão de tipo.

Armazenar cancellables em uma coleção, tipicamente um , que é deallocalizado quando o objeto proprietário (por exemplo, um controlador de visão) é deinicializado. Por exemplo:

private var cancellables = Set<AnyCancellable>()

func setupBindings() {
 publisher
 .sink { [weak self] value in
 self?.updateUI(value)
 }
 .store(in: &cancellables)
}

Você também pode chamar manualmente em um cancellable, que termina a assinatura e libera recursos.

Exemplo prático completo: Pedido de rede com atualizações de UI

Vamos construir um cenário real: buscar dados de usuários de uma API, decodificar e atualizar uma visão SwiftUI. Este exemplo demonstra o gerenciamento de erros, operadores e threads.

import SwiftUI
import Combine

struct User: Decodable, Identifiable {
 let id: Int
 let name: String
 let email: String
}

class UserViewModel: ObservableObject {
 @Published var users: [User] = []
 @Published var isLoading = false
 @Published var errorMessage: String? = nil

 private var cancellables = Set<AnyCancellable>()

 func fetchUsers() {
 isLoading = true
 errorMessage = nil

 guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else {
 errorMessage = "Invalid URL"
 isLoading = false
 return
 }

 URLSession.shared.dataTaskPublisher(for: url)
 .map { $0.data }
 .decode(type: [User].self, decoder: JSONDecoder())
 .receive(on: DispatchQueue.main)
 .sink(receiveCompletion: { [weak self] completion in
 self?.isLoading = false
 switch completion {
 case .finished:
 break
 case .failure(let error):
 self?.errorMessage = error.localizedDescription
 }
 }, receiveValue: { [weak self] users in
 self?.users = users
 })
 .store(in: &cancellables)
 }
}

Pontos-chave:

  • extrai os dados brutos da tupla de resposta.
  • usa o operador integrado da Combine para decodificar o JSON. Se a decodificação falhar, o gasoduto termina com um erro.
  • garante que as atualizações da interface de usuário ocorram no tópico principal.
  • A reserva da subscrição em liga o seu ciclo de vida ao modelo de visualização.
  • O erro é exibido através de propriedade , que pode ser ligado a um alerta na SwiftUI.

Combine e SwiftUI: Uma parceria natural

A arquitetura reativa da SwiftUI depende fortemente da Combine. Propriedades anotadas com criar editores que automaticamente notificam SwiftUI quando eles mudam. Combine operadores permitem transformar dados assíncronos antes que ele atinja a visualização. Por exemplo, você pode desbotar a entrada de um campo de texto, então buscar resultados de pesquisa e atualizar a UI usando o ou .

Usar Combine com SwiftUI elimina a necessidade de padrões manuais de KVO ou delegar, resultando em código mais previsível e testável.

Combine vs. Outros Frameworks Reativos

Antes de combinar, os desenvolvedores iOS frequentemente usavam frameworks de terceiros como RxSwift ou ReactiveCocoa[]. Combine oferece várias vantagens: integração de primeira parte, sem gerenciamento de dependência, interoperabilidade perfeita com SwiftUI e Foundation e segurança de tipo forte. No entanto, RxSwift tem um conjunto maior de operadores e um ecossistema mais maduro. Se você está começando um novo projeto que tem como alvo iOS 13+, Combine é a escolha recomendada. Para projetos que suportem versões mais antigas do sistema operacional, RxSwift continua sendo uma opção viável.

Melhores práticas para usar Combine

  • Mantenha pipelines simples – Cadeias complexas podem ser difíceis de depurar. Quebre-as em publicadores menores, com nomes, se necessário.
  • Gerir as assinaturas cuidadosamente – Armazenar sempre cancellables e evitar reter ciclos usando em encerramentos que capturam ].
  • Use agendadores apropriados – Use para mudar para a fila principal para atualizações de UI, e para realizar trabalhos pesados em filas de fundo.
  • Preferência ] para ligações simples de propriedade – Reduz o código da placa de caldeira.
  • Teste seus pipelines – A testabilidade da Combine é excelente porque você pode criar editores com ou e verificar saídas usando expectativas.
  • Cuidado com a contrapressão – O sistema de demanda da Combine garante que os assinantes controlem o fluxo. Operadores como ou podem ajudar quando os dados chegam mais rápido do que podem ser processados.

Conclusão

A estrutura Combine revoluciona como os desenvolvedores iOS lidam com eventos assíncronos e fluxos de dados. Ao entender os editores, assinantes e operadores, você pode construir pipelines concisos e reativos que são mais fáceis de raciocinar e manter. Comece com exemplos simples, como responder a notificações ou obter dados de rede, e então explore gradualmente operadores avançados como , , e padrões de recuperação de erros. Combine é uma pedra angular do desenvolvimento iOS moderno, especialmente quando emparelhado com SwiftUI. Dominá-lo fará você um desenvolvedor mais eficaz e permitirá criar aplicativos que respondem fluidamente às interações do usuário e fontes de dados externas.

For further reading, consult the official Apple Combine documentation, Hacking with Swift’s Combine introduction, or Ray Wenderlich’s Combine book for deeper dives.