Search code examples
swiftreactivereactive-swift

Given a list of timers, how to output if one of them completed, while also being able to reset the list?


I have an output signal that should output when one of a given set of timers time out, complete or when the entire list is reset.

enum DeviceActionStatus {
    case pending
    case completed
    case failed
}

struct DeviceAction {

    let start: Date
    let status: DeviceActionStatus 
    func isTimedOut() -> Bool // if start is over 30 seconds ago
    let id: String

}

Output signal:

let pendingActionUpdated: Signal<[DeviceAction], NoError>

Inputs:

let completeAction: Signal<String, NoError>
let tick: Signal<Void, NoError>  // runs every 1 second and should iterate to see if any DeviceAction is timed out
let addAction: Signal<DeviceAction, NoError> 
let resetAllActions: Signal<Void, NoError>

It should output an array of all running device actions.

let output = Signal.combineLatest( 
                     addAction,
                     resetAllActions,
                     tick,
                     Signal.merge(
                          completeAction,
                          tick.take(first: 1).map { _ in "InvalidActionId" }
                     )) // make sure the combinelatest can fire initially 

I've tried sending this to a .scan to cumulate every time the addAction is fired, and resetting every time the resetAllActions is fired, but since there is no way of knowing which of those fired, i can't get the logic to work. How can I both cumulate a growing list while also being able to run through it and being able to reset it when I want?


Solution

  • This looks like a job for the merge/enum pattern. I'm more an RxSwift guy myself, but if you map each of your Signals into an enum and merge them, then you can receive them properly into your scan...

    enum ActionEvent {
        case complete(String)
        case tick
        case add(DeviceAction)
        case reset
    }
    
    merge(
        completeAction.map { ActionEvent.complete($0) },
        tick.map { ActionEvent.tick },
        addAction.map { ActionEvent.add($0) },
        resetAllActions.map { ActionEvent.reset }
    ).scan([DeviceAction]()) { actions, event in 
        switch event {
        case let .complete(id):
            return actions.filter { $0.id != id }
        case .tick:
            return actions.filter { $0.isTimedOut() == false }
        case let .add(action):
            return actions + [action]
        case .reset:
            let resetDate = Date()
            return actions.map { $0.start = resetDate }
            // or
            return []
            // depending on what "reset" means.
    }