Introducción a la programación reactiva con Combine en iOS

Este programador de aplicaciones iOS es intrínsecamente asincrónico. Desde solicitudes de red y consultas de bases de datos a eventos de interfaz de usuario y datos de sensores, los desarrolladores deben gestionar un flujo constante de información que llegue a tiempos impredecibles. Tradicionalmente, el manejo de estos eventos requiere delegados, callbacks o centros de notificación, que a menudo llevaron a un código complejo y prono de errores.

¿Cuál es el Marco Combinado?

Combinar es un marco nativo Swift que proporciona una API declarativa para procesar valores a lo largo del tiempo. Es parte del ecosistema de Apple e integra perfectamente con SwiftUI, Foundation y UIKit. Combinar se construye en tres abstracciones básicas: publicadores emiten valores [Fopera:2]subscribentes [LT]

A diferencia de patrones anteriores como o KVO, Combine ofrece seguridad compilada, inferencia de tipo y composición poderosa. Es similar en concepto a extensiones reactivas como RxSwift pero está integrado de primera y profunda con las plataformas de Apple. Entendimiento Combinar es esencial para desarrolladores iOS modernos, especialmente aquellas aplicaciones de construcción con SwiftUI, donde Combine potencia muchas de las características impulsadas por datos.

Conceptos básicos de la combinación

Antes de bucear en código, es crítico captar los cuatro componentes primarios que forman cada tubería Combine:

  • Publisher] – Un tipo que puede emitir una secuencia de valores con el tiempo. Declara qué tipo de valores produce () y qué tipo de errores puede fallar con (). Un editor puede emitir un número arbitrario de valores, luego eventualmente completo o fallado.
  • Subscriber] – Un tipo que recibe valores de un editor. Define tres métodos: , (para manejar un nuevo valor), y (para manejar el éxito o el fracaso). La mayoría de los desarrolladores utilizan suscriptores listos como o ]
  • Operator] – Un editor que transforma valores de un editor de corriente avanzada. Operadores como , , , y son ellos mismos editores que se suscriben a editores de corriente, aplican una transformación y repubúbren el resultado.
  • Subscripción] – La conexión entre un editor y un suscriptor. Una suscripción controla el flujo de demanda (represión). Las suscripciones pueden ser canceladas para dejar de recibir valores, que se maneja automáticamente cuando un es desaltado o cancelado manualmente.

Además, Combine proporciona protocolo (y el tipo de hormigón ) para gestionar el ciclo de vida de una suscripción. Guardar cancellables en un conjunto (a menudo ) asegura que las suscripciones se derriten automáticamente cuando el objeto de propiedad se desalienta.

Creando Publishers

Combine proporciona muchos editores incorporados que cubren tareas asincrónicas comunes. También puede crear editores personalizados mediante la implementación del protocolo o el uso de inicializadores de conveniencia.

Timer Publisher

Use para emitir la fecha actual a intervalos regulares. Para comenzar el temporizador inmediatamente, llame :

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

El editor emite valores cada segundo en el bucle principal de la carrera.

URLSession Data Task Publisher

Combine extiende con un editor que realiza solicitudes de red:

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

Este editor emite un tuple al terminar, o un al fracaso.

NotificationCenter Publisher

Convertir cualquier notificación en un editor:

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

Útil para reaccionar a los eventos del sistema sin agregar manualmente observadores.

Just and Future

emite un único valor y luego completa inmediatamente. Es perfecto para probar o envolver una constante en un editor:

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

es un editor que eventualmente produce un valor único (o falla) de forma asincrónica. Ejecute el cierre proporcionado exactamente una vez y encaje el resultado para los suscriptores posteriores:

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

PasoAvanzadoSujeto y Valor actual

Los sujetos actúan como editor y suscriptor, lo que le permite inyectar valores manualmente en un flujo. transmite valores a los suscriptores sin retener un valor actual; mantiene el valor más reciente y lo vuelve a reproducir a los nuevos suscriptores:

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

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

Suscribiendo a un Editor

Para activar un oleoducto, necesita un suscriptor. Los más utilizados son y .

El Suscriptor de Sink

proporciona un manejo basado en el cierre de los valores recibidos y los eventos de terminación:

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

También puede manejar errores proporcionando un cierre :

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)")
 })

El Suscriptor de la Assignia

une la producción del editor a una propiedad de un objeto, opcionalmente a través de un camino clave. La propiedad debe estar marcada con o ser compatible con 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"

Suscriptor personalizado

Para casos de uso avanzado, puede implementar directamente el protocolo . Sin embargo, esto es raramente necesario; y cubren la mayoría de los escenarios.

Transformación de datos con operadores

Los operadores son la columna vertebral de Combine, lo que le permite componer tuberías complejas de bloques de construcción simples. Aquí están los más esenciales:

Mapa

Transforma cada valor emitido mediante un cierre:

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

Filtro

Pasa a través de valores que satisfacen un predicado:

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

FlatMap

Aplana múltiples editores internos en una sola secuencia. Comúnmente utilizado cuando cada valor desencadena una nueva operación asincrónica:

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

CombineLatest

Combina los valores más recientes de dos o más editores en un tuple. Emite un nuevo valor cada vez que cualquiera de los editores emite:

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

Zip

Pares valores de múltiples editores en orden, esperando a que todos los editores emitan el siguiente valor antes de emitir el par:

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

Merge

Interleaves valores de múltiples editores del mismo tipo en una sola secuencia:

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

Coleccionista

Valores de amortiguación en arrays, emitiendo una vez que el buffer se llena o el flujo completa:

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

Debounce y Throttle

espera una pausa especificada entre los valores antes de emitir el último. emite a la mayoría de un valor por intervalo de tiempo. Ambos son esenciales para manejar eventos de alta frecuencia como entrada de texto:

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

Manejo de errores en la combinación

El tipo de un editor define los errores que puede emitir. Cuando un editor falla, la suscripción se termina. Utilizar operadores de gestión de errores para recuperar o transformar errores.

PruebeMap y pruebaFilter

Si su cierre de transformación puede lanzar, use o . El oleoducto falla si se lanza un error:

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

Atrapa.

Reemplazar un fallo con un editor alternativo. Útil para proporcionar valores de retroceso:

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

Retrocedimiento

Resuscribe automáticamente al editor de corriente arriba un número específico de veces si falla:

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

reemplazarError y reemplazar a Empty

reemplaza un fallo con un valor dado. proporciona un valor si el editor completa sin emitir nada.

Gestión de memoria y cancelación

Cada suscripción consume recursos y debe ser cancelada correctamente para evitar las fugas de memoria. Combine proporciona , una referencia cancelable tipo-estado.

Almacene las cancelaciones en una colección, típicamente un , que se desalienta cuando el objeto de propiedad (por ejemplo, un controlador de vista) es deinicializado. Por ejemplo:

private var cancellables = Set<AnyCancellable>()

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

También puede llamar manualmente sobre un cancelable, que termina la suscripción y libera recursos.

Ejemplo práctico completo: Solicitud de red con actualizaciones de interfaz de usuario

Construyamos un escenario real: buscar datos de usuario de una API, decodificarlo y actualizar una vista SwiftUI. Este ejemplo demuestra el manejo de errores, operadores y gestión de hilos.

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)
 }
}

Puntos clave:

  • extrae los datos brutos del tuple de respuesta.
  • utiliza el operador incorporado de Combine para decodificar JSON. Si falla la decodificación, el oleoducto termina con un error.
  • asegura que las actualizaciones de la interfaz de usuario ocurran en el hilo principal.
  • Cumplir la suscripción en vincula su ciclo de vida al modelo de vista.
  • El error se supera mediante propiedad, que puede estar vinculado a una alerta en SwiftUI.

Combinación y SwiftUI: Una asociación natural

La arquitectura reactiva de SwiftUI depende en gran medida de Combine. Propiedades anotadas con crear editores que notificarán automáticamente a SwiftUI cuando cambien. Combinar operadores le permiten transformar datos asincrónicos antes de que llegue a la vista. Por ejemplo, puede desprestigiar la entrada de un campo de texto, luego buscar resultados de búsqueda y actualizar la UI utilizando [74 [LT]

Usando Combine con SwiftUI elimina la necesidad de patrones manuales de KVO o delegado, lo que resulta en un código más predecible y testable.

Combine vs. Other Reactive Frameworks

Antes de Combine, los desarrolladores de iOS a menudo utilizan marcos de terceros como RxSwift o ReactiveCocoa. Combinar ofrece varias ventajas: integración de primer partido, no gestión de dependencia, interoperabilidad sin problemas con SwiftUI y Foundation, y seguridad de tipo fuerte.

Las mejores prácticas para usar Combine

  • Mantén los oleoductos simples] – Las cadenas complejas pueden ser difíciles de depurar. Rompelos en editores más pequeños, llamados si es necesario.
  • Gestionar las suscripciones cuidadosamente – Guardar siempre los cancellables y evitar retener ciclos utilizando en los cierres que capturan .
  • Utilizar los cronogramas apropiados – Usar para cambiar a la cola principal para las actualizaciones de la interfaz de usuario, y para realizar trabajos pesados en colas de fondo.
  • Prefer para la fijación de propiedades simples] – Reduce el código de caldera.
  • Prueba tus oleoductos – La prueba de Combine es excelente porque puedes crear editores con o y verificar salidas utilizando expectativas.
  • Cuidado con la presión de la marcha – El sistema impulsado por la demanda de Combine garantiza a los suscriptores el control del flujo. Los operadores como o pueden ayudar cuando los datos llegan más rápido de lo que se puede procesar.

Conclusión

El marco Combine revoluciona cómo los desarrolladores de iOS manejan eventos y flujos de datos asincrónicos. Al entender a los editores, suscriptores y operadores, puede construir tuberías concisas y reactivas que son más fáciles de razonar y mantener. Comience con ejemplos simples como responder a notificaciones o buscar datos de red, luego explore gradualmente operadores avanzados como , y crear patrones de recuperación de errores.

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