Hello guys, I was unable to solve my problem so far, so thanks for any advice in advance!
Let's say I have two modules - Shared and Local. SharedState should act like a global state, holding playingId for the whole app lifecycle. LocalState has it's id, isPlaying boolean which should be true if playingId in SharedState (Store) equals to that LocalStateid. The problem is that there can be infinite instances of LocalState, even multiple instances with same id and I need to have isPlaying property in sync with SharedState's playingId property + LocalAction.play should map to SharedState.play(String) with LocalState.id and LocalAction.stop to SharedAction.stop. I hope my explanation is understandable, and thanks once more for any advice.
// MARK: - Shared
struct SharedState: Equatable {
var playingId: String?
}
enum SharedAction: Equatable {
case play(String)
case stop
}
let sharedReducer: Reducer<SharedState, SharedAction, Void> = .init { state, action, _ in
switch action {
case .play(let id):
state.playingId = id
case .stop:
state.playingId = nil
}
return .none
}
// MARK: - Local
struct LocalState: Equatable {
let id: String
var playing = false
var data: String?
}
enum LocalAction: Equatable {
case play
case stop
case set(String?)
}
let localReducer: Reducer<LocalState, LocalAction, Void> = .init { state, action, _ in
...
}
State that needs to be accessible through every layer of your application is best handled as a dependency in the environment that has synchronous access to the playing id, as well as endpoints for playing and stopping:
struct PlayingClient {
var id: () -> String?
var play: (String) -> Effect<Never, Never>
var stop: () -> Effect<Never, Never>
}
Then a live version of this client could manage this state:
extension PlayingClient {
static var live: Self {
var id: String?
return Self(
id: { id },
play: { newId in
.fireAndForget { id = newId }
},
stop: {
.fireAndForget { id = nil }
}
)
}
}
And then a reducer can use this dependency to play, stop or figuring out if it is currently playing:
let reducer: Reducer<State, Action, Environment> = .init { state, action, environment in
switch action {
case .onAppear:
state.isPlaying = state.id == environment.playingClient.id()
return .none
case .play(let id):
return environment.playingClient.play(id).fireAndForget()
case .stop:
return environment.playingClient.stop().fireAndForget()
}
}
And once you've done all of this you will be in a good position to write tests with a test version of the PlayingClient.
Thank you! I had something similar in my mind but couldn't get it together. I was still hanging on using state with some trickery with stores/reducers but using it as dependency in environment looks reasonable. I just edited PlayingClient a bit and reducer accordingly, so it can react to state changes
struct PlayingClient {
var id: () -> Effect<String?, Never>
var play: (String) -> Effect<Never, Never>
var stop: () -> Effect<Never, Never>
static var live: Self {
let id: PassthroughSubject<String?, Never> = .init()
return .init(
id: {
id.eraseToEffect()
},
...
)
}
}