Search code examples
swiftcombine

In Swift, how to assign values when generic parameters are different


I am trying to subscribe to multiple publishers. The Output type of publishers may not be determined.

static func listen<T>(publisher: Published<T>.Publisher){
    publisher.sink { _Arg in
        // do something
    }.store(in: &cancellables)
}
listen(publisher: env.$showMenuIcon)
listen(publisher: env.$dateFormatLunar)
listen(publisher: env.$dateFormatAd)
listen(publisher: env.$showWeek)
listen(publisher: env.$showWeather)
// in env class
@Published var timeItem = true
@Published var dateFormatAd = "yyyy-MM-dd"

Each of my publishers may have different generic parameter types, and I can only call listen by copying multiple lines of code like this. Is there any way to modify the Listen method to accept an array type? Or is there another way I can simplify my code?


Solution

  • First of all, I would advise you against your idea of a generic function for subscribing to publishers of different types. Why? Imagine that this is possible (and it is possible in principle, an example is below). How do you want to distinguish between different data types in your sink block? In my opinion, the only way to bring different data types under one roof and then still have the possibility to distinguish them from each other is to create your own data type, that is not generic. E.g. something like this:

    struct Result {
        let type: Type   // E.g. an enum of possible types.
        let value: Any
    }
    

    Then you have to look in your sink block each time for the data type of your value and casts it accordingly. In my opinion, this makes your logic very complicated. I am not a fan of universal functions. They are very often the big sources of errors.

    An example/idea for you on how to realise your wish:

    class MyClass {
        // Common data type
        typealias Value = Any
        
        @Published var numberPublisher: Value?
        @Published var stringPublisher: Value?
        @Published var booleanPublisher: Value?
        
        var subscription: AnyCancellable?
        
        init() {
            // Start listening of publishers.
            listen(publishers: [
                $numberPublisher,
                $stringPublisher,
                $booleanPublisher
            ])
        }
        
        func listen(publishers: [Published<Value?>.Publisher]) {
            let mergedPublishers = Publishers.MergeMany(publishers)
    
            subscription = mergedPublishers
                // Skipping initial nil values and just not seeing them.
                .dropFirst(publishers.count)
                .sink(receiveCompletion: { completion in
                    print("Completion \(completion)")
                }, receiveValue: { value in
                    print("Value \(String(describing: value))")
                })
        }
    }
    

    As I wrote above, I created a common data type for all my publishers. Even if your function is generic, you can not pass parameters of different types to it at the same time.

    I tested my code in the Playground:

    let myClass = MyClass()
    myClass.numberPublisher = 123
    myClass.stringPublisher = "ABC"
    myClass.booleanPublisher = false
    
    // Console output
    Value Optional(123)
    Value Optional("ABC")
    Value Optional(false)
    
    

    In your place I would subscribe to each publisher separately and directly, without any functions in between (the way you're already doing it).

    Hopefully I could help you.