Aquí están los tres puntos que quiero lograr:
Para ponerlo más en contexto:
Quiero alternar un estado en el grifo de A a B, B a C y luego volver de C a A. Sin embargo, si el estado actual es B y no hay entrada del usuario, quiero cambiar el estado a C después de un tiempo aleatorio intervalo. Si el estado actual es B y el usuario toca la pantalla, no quiero volver a cambiar el estado a C, con la excepción de que el usuario cambie con la frecuencia suficiente para volver a estar en B, en cuyo caso querría un nuevo intervalo aleatorio para ser utilizado.
Tengo un pseudo código (más o menos) para aclarar aún más mi problema.
class ViewModel { var state: State private var stateSubject: CurrentValueSubject<State, Never> private var cancellables: Set<AnyCancellable> = [] init(initialState: State) { state = initialState stateSubject = CurrentValueSubject(state) // Pipe to store new state stateSubject .sink { [unowned self] in self.state = $0 } .store(in: &cancellables) // Logic to switch specific state after interval // Approach 1 stateSubject .debounce(for: .seconds(randomInterval), scheduler: RunLoop.main) .filter { $0 == .B } .sink { [unowned self] _ in stateSubject.send(.C) } .store(in: &cancellables) // -------------------------------- // Approach 2 stateSubject .flatMap(maxPublishers: .max(1)) { state in Future { [unowned self] promise in DispatchQueue.main.asyncAfter(deadline: .now() + randomInterval) { promise(.success(state)) } } } .filter { $0 == .B } .sink { [unowned self] _ in stateSubject.send(.C) } .store(in: &cancellables) } var randomInterval: Double { Double.random(in: 1...4) } // Called on tap func toggleState() { state = state.toggle() } } extension ViewModel { enum State { case A, B, C func toggle() -> Self { switch self { case .A: return .B case .B: return .C case .C: return .A } } } } Enfoque 1: Sin embargo, esto parece razonable, ya que el operador .debounce solo se crea una vez y, por lo tanto, solo accede a randomInterval una vez, lo que da como resultado que el rebote siempre sea la misma cantidad de tiempo.
Enfoque 2: esto funciona bastante bien, excepto si el usuario cambia el estado con demasiada frecuencia. No pude encontrar la causa del problema, pero encontré que el estado alternaba incluso en State.A
Gracias por su ayuda y avíseme si tiene más preguntas.
¡Desafío aceptado! :)
import Foundation import Combine // for the example sake, // pretending that the user taps exactly every 3 seconds // use the real UI taps publisher here // it emits an integer "tap ID", you can use a tap timestamp here instead, // it just has to be unique for each tap let taps = (1...).publisher.flatMap(maxPublishers: .max(1)) { Just($0).delay(for: .seconds(3), scheduler: RunLoop.main) } // each tap timeouts emitting its tap ID after a random delay // except the first one which fires immediately with the tap let timeouts = taps.map { Just($0).delay(for: .seconds(($0 > 1) ? Double.random(in: 1...4) : 0), scheduler: RunLoop.main) }.switchToLatest() // when a new tap arrives, cancel the previous timeout // using 0, 1, 2 here instead of A, B, C for simplicity func next(_ state: Int) -> Int { (state + 1) % 3 } // given the previous state and what happened - returns the new state func update(_ prevState: Int, _ event: (Int, Int)) -> Int { let (tapID, timeoutTapID) = event let isTimeout = (tapID == timeoutTapID) let isTap = !isTimeout if isTap { print("event: tap \(tapID)") } else { print("event: timeout \(timeoutTapID)") } if isTap || (prevState == 1) { return next(prevState) } else { // ignore timeouts in other states return prevState } } let initialState = 0 let states = taps.combineLatest(timeouts) .scan(initialState, update) .removeDuplicates() let sub = states.sink { value in print("new state: \(value)") }Manifestación:
event: timeout 1 new state: 0 event: tap 2 new state: 1 event: timeout 2 new state: 2 event: tap 3 new state: 0 event: timeout 3 event: tap 4 new state: 1 event: timeout 4 new state: 2 event: tap 5 new state: 0 event: tap 6 new state: 1 event: tap 7 new state: 2 event: timeout 7 event: tap 8 new state: 0 event: tap 9 new state: 1 event: tap 10 new state: 2 event: timeout 10 event: tap 11 new state: 0 event: tap 12 new state: 1 event: tap 13 new state: 2 event: timeout 13Una desventaja es que siempre se está ejecutando un temporizador de tiempo de espera, incluso cuando el estado actual no lo necesita (ignoramos tales actualizaciones). Esperemos que sea solo un temporizador en todo momento.
Hacer que la disponibilidad del temporizador dependa del estado requiere un ciclo de retroalimentación que traté de evitar.