Search code examples
swiftreactive-swift

How to compose Actions in ReactiveSwift ("run the first enabled action from this list")


I have two Actions with the same input/output/error types, and I'd like to compose them into a single Action that runs whichever of the two is enabled (with an arbitrary tie-breaker if they both are).

Here's my first, failing, attempt:

let addOrRemove: Action<MyInput, MyOutput, APIRequestError> = Action(enabledIf: add.isEnabled.or(remove.isEnabled)) { input in
    if add.isEnabled.value {
        return add.apply(input)
    } else {
        return remove.apply(input)
    }
}

This fails because the inner add.apply(input) can't see that I checked add.isEnabled, so it wraps an additional ActionError<> layer around the error type. (This might be legit, as I'm not sure how thread-safe this approach would be, or might be a case of us knowing something the type system doesn't.) The corresponding type error is:

cannot convert return expression of type 'SignalProducer<MyOutput, ActionError<APIRequestError>>' to return type 'SignalProducer<MyOutput, APIRequestError>'

What should I do instead?


Solution

  • Github user @ikesyo provided the following answer on the ReactiveSwift issue I opened to ask the same question:

    let producer: SignalProducer<MyOutput, ActionError<APIRequestError>>
    if add.isEnabled.value {
        producer = add.apply(input)
    } else {
        producer = remove.apply(input)
    }
    return producer.flatMapError { error in
        switch error {
        case .disabled: return .empty
        case let .producerFailed(inner): return SignalProducer(error: inner)
        }
    }
    

    If they show up here I'll happily change the accepted answer to theirs (credit where it belongs).

    Warning: If I'm reading this answer correctly, it's not watertight. If add changes from enabled to disabled between the apply() and the start() of the wrapping Action, we'll get "success" (no values, but .completed) instead of the .disabled we should get. That's good enough for my use case, but YMMV.