Let's say that:
• My application is a client of a Socket server.
• I'm free to write the Socket client implementation to fit with Combine
as I prefer
I've implemented 2 solutions, one with CurrentValueSubject
(quite easy) and a second with a custom subscription and a custom publisher that I'm not sure about. I really don't know which is the best way to bridge the code that I'm using to handle server messages with Combine.
Here is my code:
To simulate the socket server I've create a fake SocketServerManager
that generates some events every N
seconds:
protocol SocketServerManagerDelegate{
func newEvent(event:String)
}
class SocketServerManager {
let timing: Double
var timerHandler:Timer? = nil
var delegates:[SocketServerManagerDelegate] = []
init(timing:Double){
self.timing = timing
}
func start(){
// Just start a timer that calls generateEvent to simulate some events
timerHandler = Timer.scheduledTimer(withTimeInterval: timing, repeats: true){
[weak self] _ in
self?.generateEvent()
}
timerHandler?.fire()
}
private func generateEvent(){
let events = ["New Player", "Player Disconnected", "Server Error"]
let currentEvent = events.randomElement
for delegate in delegates{
delegate.newEvent(event: currentEvent)
}
}
}
My custom subscription keeps a reference to an instance of the server manager and to the subscriber.
Also, it implements a SocketServerManager
delegate. So that when the server has a new event it calls the subscription that can now send the receive
event on the subscriber (This is the choice where I have A LOT OF doubts...)
class EventSubscription<S:Subscriber>:Subscription, SocketServerManagerDelegate
where S.Input == String{
private var subscriber:S?
private unowned var server:SocketServerManager
init(sub:S, server:EventsServer){
self.subscriber = sub
self.server = server
}
func request(_ demand: Subscribers.Demand) {}
func cancel() {
subscriber = nil
}
// HERE IS WHERE I SEND THE EVENT TO THE SUBSCRIBER since this subscription
is a delegate of the server manager
func newEvent(event: Event) {
_ = subscriber?.receive(event)
}
}
The publisher has nothing special...it will just create the subscription with the receive
function. Also it appends the subscription to the list of delegates registered on the server so that the generatesEvents
function can broadcast the event through the delegates (hence, through the subscriptions).
// PUBLISHER CODE ----------
func receive<S>(subscriber: S)
where S:Subscriber,
EventsPublisher.Failure == S.Failure,
EventsPublisher.Output == S.Input {
let subscription = EventSubscription(sub:subscriber, server: self.server)
server.delegates.append(subscription)
subscriber.receive(subscription: subscription)
}
What do you think about this implementation? to me it seems quite clunky, but I really don't know how to bridge the events from the Server Manager to Subscribers.
The Combine framework provides a declarative Swift API for processing values over time. These values can represent many kinds of asynchronous events. Combine declares publishers to expose values that can change over time, and subscribers to receive those values from the publishers.
What's a sink? While a complete explanation of Combine, publishers, subscribers, and sinks is beyond the scope of this article, for our purposes here it's probably enough to know that in Combine a sink is the code receives data and completion events or errors from a publisher and deals with them.
AnyPublisher is a concrete implementation of Publisher that has no significant properties of its own, and passes through elements and completion values from its upstream publisher. Use AnyPublisher to wrap a publisher whose type has details you don't want to expose across API boundaries, such as different modules.
A PassthroughSubject broadcasts elements to downstream subscribers and provides a convenient way to adapt existing imperative code to Combine. As the name suggests, this type of subject only passes through values meaning that it does not capture any state and will drop values if there aren't any subscribers set.
I would do it with the following simple & manageable approach (IMO this is not the right place for "delegate"s).
Completely testable module: consumer is SwiftUI view. Tested as worked with Xcode 11.2 / iOS 13.2, however I don't see any platform limitations.
Demo:
Here is a scratchy idea code. Please find additional comments inline.
import SwiftUI
import Combine
protocol SocketServerManagerDelegate{
func newEvent(event:String)
}
class SocketServerManager {
// transparent subject that manages subscribers/subscriptions
let publisher = PassthroughSubject<String, Never>()
let timing: Double
var timerHandler:Timer? = nil
init(timing:Double){
self.timing = timing
}
func start(){
// Just start a timer that calls generateEvent to simulate some events
timerHandler = Timer.scheduledTimer(withTimeInterval: timing, repeats: true){
[weak self] _ in
self?.generateEvent()
}
timerHandler?.fire()
}
func stop(){
publisher.send(completion: .finished) // notifies all that finished
}
private func generateEvent(){
let events = ["New Player", "Player Disconnected", "Server Error"]
guard let currentEvent = events.randomElement() else { return }
publisher.send(currentEvent) // send to all subscribers
}
}
// usage
class ViewModel: ObservableObject {
private let server = SocketServerManager(timing: 1)
private var cancellables = Set<AnyCancellable>()
func setup() {
guard cancellables.isEmpty else { return } // already set up
// add one example subscriber
server.publisher
.assign(to: \.value1, on: self)
.store(in: &cancellables)
// add another example subscriber
server.publisher
.sink(receiveValue: { value in
self.value2 = value
})
.store(in: &cancellables)
server.start()
}
@Published var value1: String = "<unknown>"
@Published var value2: String = "<unknown>"
}
// view demo
struct TestSocketServerPublisher: View {
@ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
Text("Observer1: \(viewModel.value1)")
Divider()
Text("Observer2: \(viewModel.value2)")
}
.onAppear {
self.viewModel.setup()
}
}
}
struct TestSocketServerPublisher_Previews: PreviewProvider {
static var previews: some View {
TestSocketServerPublisher()
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With