How to send back an action from a view store publisher?

I have a button that sends a rewind command... which sets a shouldRewind flag in my state.

My view controller subscribes to a view store.publisher for shouldRewind and rewind something if true.

I tried sending back an action after rewinding to reset the shouldRewind state to false but I get hit by this dreadful assertion error:

      * The store was sent an action recursively. 

I don't seem to be sending that action recursively.

      * The store has been sent actions from multiple threads. 

All actions are sent from the main thread.

I am really confused.

I have dropped the second action and I reset the flag right away with an effect.

        case .shouldRewind(let flag):
            state.shouldRewind = flag

            return { environment in
                guard flag else { return .none }

                return Effect(value: .shouldRewind(false))
                    .eraseToEffect()
            }
        }

I am still curious as to why my naive approach didn't work.

You almost certainly are sending an action recursively. The store's send(_:) method calls out to the reducer. As the reducer returns, it updates the store's state, which publishes an output. You're subscribed to that publisher, and in your subscriber you're calling back into send(_:).

Put a breakpoint on the assertionFailed in send(_:) and try again. When the breakpoint is hit, check the stack trace. If you see two stack frames calling send(_:), then you are re-entering send(_:) while it is updating state.

Thanks, what's the proper way to do it?

It looks like your rewrite is a correct solution to fixing the recursive send problem.

However, I guess you're toggling shouldRewind to trigger a side effect in your view controller. You should probably be performing that side effect via an Effect returned by your reducer.

I'm guessing you have a setup like this:

  • You have a video (or audio) player managed by the view controller.
  • A tap on the rewind button should tell the video player to rewind.
  • Neither the video player object nor the current playback time are part of the store's state.

Here's a way to make the rewind happen in an Effect. Create a rewind action that includes the video player as an associated value:

enum VideoPlayerAction {
    case rewind(VideoPlayer)
    ...
}

With this action, your reducer can create an Effect that rewinds the video player:

switch action {
case .rewind(let player):
    return .fireAndForget { player.rewind() }
    // or maybe
    return .fireAndForget { environment.rewind(player) }
}

Thanks @mayoff

Protocols don't play really nice with Equatable.

If VideoPlayer is a protocol how can I keep AppAction as an Equatable?

public protocol Rewindable {
    func rewind(animated: Bool)
}

Adding protocol performance to the protocol we get the following error:

Protocol 'Rewindable' can only be used as a generic constraint because it has Self or associated type requirements

It doesn't seem like it is possible to make a property-less protocol conform to Equatable.

I don't even seem to be able to pass a closure instead of VideoPlayer...

Your second suggestion was to use the environment... how do I set environment.rewind if I can't have access to rewind until the app is running? Is there a mechanism to update the environment?

@stephencelis What is your opinion and guidance on this?

Should a view controller be sending back some code to execute in the reducer? It doesn't seem right...

Terms of Service

Privacy Policy

Cookie Policy