Hey @tgrapperon! It's been a while but I finally managed to progress with this topic. With your pointers and some time, I finally got my sample project to the state I hoped it to be. For documentation for me and others with the same question, I'll outline my solution here.
The "LongLiving" case study proved to be a helpful example. I nevertheless had some modifications to make – and to learn about the modern concurrency mechanisms, having not had the opportunity to use them before. I created a ConnectionController
wrapper class that instantiates the PolarService
as a private property. Calls to connect()
and disconnect()
are then simply forwarded to that.
The isDeviceConnected
property however got wrapped like this
var connectionChanged: AsyncStream<Bool> {
AsyncStream { continuation in
cancellable = self.service.$isDeviceConnected.sink { connected in
continuation.yield(connected)
}
}
}
I have to admit I don't understand why the @Sendable () async ->
part of the signature in the "LongLiving" example is necessary – for me, that just makes the return type incompatible.
Looking at the Reducer
counterpart of this, I added my ConnectionController as a @Dependency
in the feature struct and introduced a case connectionStatusChanged(Bool)
and a connectionTask
action. Here they are for reference:
case .connectionTask:
return .run { send in
for await isConnected in self.polarService.connectionChanged {
await send(.connectionStatusChanged(isConnected))
}
}
case let .connectionStatusChanged(connected):
state.connected = connected
return .none
Here, we extract any value changes out of the AsyncStream
using a for-in loop over it that is simply sending newly received values to another action handler. That construct itself is wrapped in a .run
Effect that gets kicked of by attaching this modifier to a view inside the WithViewStore
helper:
.task {
viewStore.send(.connectionTask)
}
Please note that I left of the .finish()
modifier found in the "LongLiving" example. The reason is that I want to keep listening to value changes even when a new view gets pushed over my current view. In fact, that's the whole idea here:
NavigationLink(isActive: .constant(viewStore.connected)) {
DataListView(store: self.store.scope(state: \.dataList,
action: Start.Action.dataList))
} label: {
EmptyView()
}
When we are connected, I want to show a different view, but automatically pop back when disconnected; be it by the device being turned off or by the user tapping "Disconnect" manually. I found that events would no longer be received once the DataListView was shown, but that removing finish()
changed that. Wouldn't have guessed that by the docs, still not sure if it's intended to be used like this.
Anyway, that outlines my solution. Feel free to let me know if you consider this approach "proper" or let me know what you would change if it's too hacky.