DEV Community

GoyesDev
GoyesDev

Posted on

[SC] Migrando desde programación reactiva (RxSwift y Combine)

Comprensión durante la lectura

¿Por qué la comunidad Swift está migrando de RxSwift y Combine?

Combine está recibiendo menos actualizaciones y la comunidad de programación reactiva está perdiendo popularidad.

¿Existe alguna alternativa nativa en Swift 6 para la observación reactiva de valores?

Existe una propuesta en curso SE-475 Transactional Observation of Values que técnicamente permite observar cambios sobre un atributo y procesarlos como si fuera un AsyncSequence:

let names = Observations { person.name }

Task.detached {
  for await name in names {
    print("Name 1 was updated to: \(name)")
  }
}


Task.detached {
  for await name in names {
    print("Name 2 was updated to: \(name)")
  }
}
Enter fullscreen mode Exit fullscreen mode

¿Cómo se implementa el debouncing sin Combine?

$searchQuery
  .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
  .sink(receiveValue: { [weak self] query in
    guard !query.isEmpty else {
      self?.searchResults = Self.articleTitlesDatabase
    return
  }

  /// A simplified static result and search implementation.
  self?.searchResults = Self.articleTitlesDatabase
    .filter { $0.lowercased().contains(query.lowercased()) }
  })
  .store(in: &cancellables)
Enter fullscreen mode Exit fullscreen mode

¿Cómo funciona el reemplazo de un pipeline de debounce usando Task.sleep y cancelación manual?

/// Using manual cancellation management.
func search(_ query: String) {
  /// Cancel any previous searches that might be 'sleeping'.
  currentSearchTask?.cancel()

  currentSearchTask = Task {
    do {
      /// Debounce for 0.5 seconds to wait for a pause in typing before executing the search.
      try await Task.sleep(for: .milliseconds(500))

      print("Starting to search!")

      guard !query.isEmpty else {
        searchResults = Self.articleTitlesDatabase
        return
      }

      /// A simplified static result and search implementation.
      searchResults = Self.articleTitlesDatabase
        .filter { $0.lowercased().contains(query.lowercased()) }
    } catch {
      print("Search was cancelled!")
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Problema con el NotificationCenter

NotificationCenter.default.publisher(for: .someNotification)
  .sink { [weak self] _ in
    self?.didReceiveSomeNotification()
  }.store(in: &cancellables)
Enter fullscreen mode Exit fullscreen mode

Notifications today rely on an implicit contract that an observer’s code block will run on the same thread as the poster, requiring the client to look up concurrency contracts in documentation, or defensively apply concurrency mechanisms which may or may not lead to issues. Notifications do allow the observer to specify an OperationQueue to execute on, but this concurrency model does not provide compile-time checking and may not be desirable to clients using Swift Concurrency.

Se espera que el observador del NotificationCenter corra en el mismo hilo que emite la notificación.

¿Por qué los closures de sink no ofrecen seguridad en tiempo de compilación con actores?

Combine no está integrado con el sistema de concurrencia de Swift 6, por lo que el compilador no puede verificar si el código dentro de un sink es seguro respecto al aislamiento de actores.

NotificationCenter.default.publisher(for: .someNotification)
  .sink { [weak self] _ in
    self?.didReceiveSomeNotification()
    // ✅ Compila sin errores... pero puede crashear
  }.store(in: &cancellables)
Enter fullscreen mode Exit fullscreen mode
// ❌ El compilador advierte que esto es inseguro
NotificationCenter.default.addObserver(...)
Enter fullscreen mode Exit fullscreen mode

El closure de sink es esencialmente una caja negra para el compilador en términos de concurrencia. Swift no sabe en qué hilo se va a ejecutar ese closure, por lo que no puede garantizar que respete el aislamiento del @MainActor.

El riesgo concreto es el siguiente: si una notificación es enviada desde un Task.detached (fuera del hilo principal), el closure de sink se ejecutará en ese mismo hilo, violando el aislamiento del actor y produciendo un crash en tiempo de ejecución, no un error en tiempo de compilación.


Recordar sin releer

¿Cuál es el riesgo de threading cuando se usa sink junto con actores de Swift Concurrency?

¿Por qué receive(on:) no es suficiente para resolver problemas de aislamiento de actores?

¿Cuándo tiene sentido quedarse con Combine en lugar de migrar?


Revisión y síntesis

¿Cuáles son las dos soluciones propuestas para manejar NotificationCenter con actores?

¿Qué ventaja concreta ofrece reescribir código en Swift Concurrency respecto a la seguridad en tiempo de compilación?

En tu opinión, ¿los argumentos del autor son suficientes para justificar una migración completa en un proyecto grande?


Bibliografía

Top comments (0)