Software Engineering and Programming
How to Use Combine Framework for Reactive Programming in Ios
Table of Contents
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.