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.
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) }
}
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...
I think the problem you're having here is because you're trying to control something from your reducer which it has no control over - your VideoPlayer is not part of your state or your environment. From how you've described it, it seems to be more equivalent to a view, so I would think of it that way and let changes to your state drive the VideoPlayer inside the view it's wrapped. When shouldRewind == true, trigger the rewind on your VideoPlayer inside the view then fire another action to notify that it has been rewound and update your state again.
Thanks and that's exactly what I was trying to do earlier and ended up calling back into send(_:) in my subscriber and I also have no way to tell when the view is rewound...
I guess I can just fire a onRewind action right away:
case .rewind:
if !state.shouldRewind {
state.shouldRewind.toggle()
}
return { _ in .init(value: .onRewind) }
case .onRewind:
if state.shouldRewind {
state.shouldRewind.toggle()
}
return { _ in .none }
}
and in my view :
viewStore.publisher.shouldRewind
.sink { [weak self] ok in
if ok { self?.rewind() }
}.store(in: &cancellables)
That’s not quite what I was suggesting. You shouldn’t need to explicitly subscribe to the publisher in this way. Just do a simple conditional check of your state in your view to determine if you need to trigger the rewind, the view will re-render when the state changes.
@lukeredpath , I have no easy way to check if the player has done the rewind except in the subscription but inside the subscription I am not allowed to send an action.
Also I am using UIKit so I have to subscribe explicitly in viewDidLoad.
@mycroftcanner could you show how would look your final code? I also have some similar scenario that I'm trying to send an action in inside the subscription ( I put my subscription in the environment object). And I have no idea how to solve it...