Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Combine Subscriptions, right flow and architectural choices

Tags:

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)
        }
    }            
}

Custom Publisher and Subscription

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.

like image 457
MatterGoal Avatar asked Dec 15 '19 16:12

MatterGoal


People also ask

What is combine framework in Swift?

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 is sink in combine?

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.

What is AnyPublisher in Swift?

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.

What is PassthroughSubject?

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.


1 Answers

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:

SockerServer emulate Combine 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()
    }
}
like image 57
Asperi Avatar answered Oct 11 '22 22:10

Asperi