DEV Community

David Goyes
David Goyes

Posted on

Combine #11: Temporizadores

RunLoop

Todo hilo creado con la clase Thread puede tener su propio RunLoop: Foundation lo puede crear al invocar RunLoop.current desde un hilo. Sin embargo, la clase RunLoop no es "thread-safe", razón por la cual no se debería invocar métodos del RunLoop de otro hilo distinto de current. Además, a no ser de que se conozca bien cómo opera un RunLoop, es mejor usar solo el RunLoop del hilo principal (RunLoop.main).

Se puede usar el RunLoop para crear un timer con el método schedule(after:interval:tolerance:) que es ofrecido por Combine, en el protocolo Scheduler, y permite crear un temporizador. Este método devuelve una suscripción (Cancellable) que puede ser usada para cancelar el temporizador.

let runLoop = RunLoop.main
let subscription = runLoop.schedule(
  after: runLoop.now,
  interval: .seconds(1),
  tolerance: .milliseconds(100)
) {
  // Scheduler define varios métodos de bajo nivel, y uno que permite crear temporizadores cancelables.
  print("Timer fired")
}
runLoop.schedule(after: .init(Date(timeIntervalSinceNow: 3.0))) {
  print("Cancel timer")
  subscription.cancel()
}
Enter fullscreen mode Exit fullscreen mode

Clase Timer

Se puede crear un ConnectablePublisher de la clase Timer que agrega un evento a un RunLoop determinado, por medio del método estático publish(every:tolerance:on:in:options:):

let publisher = Timer.publish(every: 1.0, on: .main, in: .common)
Enter fullscreen mode Exit fullscreen mode

El ConnectablePublisher es un tipo de Publisher que empieza a emitir eventos cuando se invoca el método connect() sobre él. Si se invoca autoconnect(), entonces el Publisher empieza a emitir tan pronto ocurre la primera suscripción.

Si el Publisher.Output del Timer es Date, entonces emitirá la fecha actual en cada evento. Para acoplar un acumulador/contador al Timer se puede usar el siguiente bloque de código, donde scan acumula valores en cada emisor del temporizador:

let subscription = Timer
  .publish(every: 1.0, on: .main, in: .common)
  .autoconnect()
  .scan(0) { counter, _ in 
    counter + 1 
  }
  .sink { ... }
Enter fullscreen mode Exit fullscreen mode

El RunLoop no dejará de hacer su tarea inmediatamente para atender el Timer, así que por medio del parámetro tolerance se le especifica cuánto tiempo máximo se lo puede esperar antes de que atienda a nuestro temporizador.

DispatchQueue

La clase DispatchQueue, igual que RunLoop, también conforma el protocolo Scheduler así que también puede ejecutar una tarea de forma periódica con el método schedule(after:interval:tolerance:).

let queue = DispatchQueue.main
let cancellable = queue
  .schedule(after: queue.now,
            interval: .seconds(1)) {
    print("Timer Fired")
  }
queue.schedule(after: .init(.now() + .seconds(3))) {
  print("Cancel timer")
  cancellable.cancel()
}
Enter fullscreen mode Exit fullscreen mode

Para que un Scheduler como DispatchQueue o RunLoop pueda emitir eventos, debemos usar un PassthroughSubject como pasamanos:

let source = PassthroughSubject<Int, Never>()
var counter = 0
let queue = DispatchQueue.main
let cancellable = queue
  .schedule(after: queue.now,
            interval: .seconds(1)) {
    source.send(counter)
    counter += 1
  }
let subscription = source.sink { ... }
Enter fullscreen mode Exit fullscreen mode

Cuestionario

1. ¿Por qué no se deben invocar métodos del RunLoop desde un hilo diferente al actual?

2. ¿Qué ventaja ofrece el método schedule(after:interval:tolerance:) del protocolo Scheduler en comparación con usar directamente un Timer?

3. Explica qué hace el método scan en el ejemplo del Timer.publish.

4. ¿Qué sucede si el RunLoop está ocupado cuando debe ejecutar un Timer y qué papel juega el parámetro tolerance?

5. ¿Cuál es la diferencia entre usar connect() y autoconnect() en un Timer.publish?

6. ¿Qué sucede cuando se llama a RunLoop.current dentro de un hilo nuevo?

  • [ ] Crea un nuevo RunLoop para el hilo
  • [ ] Devuelve el RunLoop del hilo principal
  • [ ] Lanza una excepción
  • [ ] Detiene la ejecución del hilo

7. ¿Qué tipo de objeto devuelve el método schedule de un RunLoop o DispatchQueue?

  • [ ] Un Timer
  • [ ] Un Subscription
  • [ ] Un Cancellable
  • [ ] Un ConnectablePublisher

8. En el ejemplo de Timer.publish, ¿qué tipo de valor emite el Publisher en cada evento?

  • [ ] Un número entero incremental
  • [ ] Una cadena con la hora
  • [ ] Un valor de tipo Date
  • [ ] Un valor de tipo TimeInterval

9. ¿Qué condición es necesaria para que un ConnectablePublisher empiece a emitir eventos?

  • [ ] Llamar a connect() o usar autoconnect()
  • [ ] Crear un RunLoop
  • [ ] Configurar una DispatchQueue
  • [ ] Agregar un PassthroughSubject

10. En el ejemplo con DispatchQueue, ¿por qué se usa un PassthroughSubject junto con el Scheduler?

  • [ ] Para convertir la cola en un Publisher que emita valores
  • [ ] Para cancelar automáticamente el temporizador
  • [ ] Para limitar la frecuencia de ejecución
  • [ ] Para evitar bloqueos del hilo principal

Solución

1. ¿Por qué no se deben invocar métodos del RunLoop desde un hilo diferente al actual?

No es thread-safe.

2. ¿Qué ventaja ofrece el método schedule(after:interval:tolerance:) del protocolo Scheduler en comparación con usar directamente un Timer?

schedule(after:interval:tolerance:) devuelve una suscripción que permite cancelar el temporizador.

3. Explica qué hace el método scan en el ejemplo del Timer.publish.

Acumula valores y los emite cada vez que recibe un Date emitido por Timer. Aparenta ser un contador entero ascendente en pasos de 1.

4. ¿Qué sucede si el RunLoop está ocupado cuando debe ejecutar un Timer y qué papel juega el parámetro tolerance?

El RunLoop va a terminar la tarea que está haciendo y luego ejecuta el método del Timer, y trata de respetar la tolerancia dada por el temporizador, a no ser que sea menor que la mínima tolerancia (minimumTolerance) del RunLoop.

5. ¿Cuál es la diferencia entre usar connect() y autoconnect() en un Timer.publish?

autoconnect() inicia el temporizador tan pronto ocurre la primera suscripción. connect() inicia el temporizador en ese momento.

6. ¿Qué sucede cuando se llama a RunLoop.current dentro de un hilo nuevo?

  • [✅] Crea un nuevo RunLoop para el hilo

7. ¿Qué tipo de objeto devuelve el método schedule de un RunLoop o DispatchQueue?

  • [✅] Un Cancellable

8. En el ejemplo de Timer.publish, ¿qué tipo de valor emite el Publisher en cada evento?

  • [✅] Un valor de tipo Date

9. ¿Qué condición es necesaria para que un ConnectablePublisher empiece a emitir eventos?

  • [✅] Llamar a connect() o usar autoconnect()

10. En el ejemplo con DispatchQueue, ¿por qué se usa un PassthroughSubject junto con el Scheduler?

  • [✅] Para convertir la cola en un Publisher que emita valores

Top comments (0)