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
.