Search code examples
swiftcastingcombine

How do I create a PassthroughSubject that can have an output of any type?


I would like to create a PassthroughSubject object that can send an output of any type. In code I currently have something like this:

let cmd1Subj = PassthroughSubject<String, Never>()
let cmd2Subj = PassthroughSubject<String, Never>()
var desiredCmd: PassthroughSubject<String, Never>?
let executeDesiredCmdSubj = PassthroughSubject<String, Never>()
var arg: String?

func executeDesiredCmd(cmdArg: String) -> AnyPublisher<String, Never> {
    guard (desiredCmd != nil) else {
        return Just("Nothing to execute\n").eraseToAnyPublisher()
    }
    desiredCmd?.send(cmdArg)
    return Just("Executed: \(String(describing: desiredCmd)) with argument: \(cmdArg)").eraseToAnyPublisher()
}

let cancellable = executeDesiredCmdSubj
    .flatMap(executeDesiredCmd)
    .receive(on: DispatchQueue.main)
    .sink(receiveValue: {
        print($0)
    })

desiredCmd = cmd1Subj
arg = "This is the argument for command 1"
desiredCmd?.send(arg!)
desiredCmd = cmd2Subj
arg = "This is the argument for command 2"
desiredCmd?.send(arg!)

How do I change desiredCmd and executeDesiredCmdSubj such that they can send an output of any type, as determined at runtime? I'd like to do something like this:

let cmd1Subj = PassthroughSubject<Int, Never>()
let cmd2Subj = PassthroughSubject<String, Never>()
var desiredCmd: PassthroughSubject<Some_Generic_Type, Never>?
let executeDesiredCmdSubj = PassthroughSubject<Some_Generic_Type, Never>()
var arg: Some_Generic_Type?

func executeDesiredCmd(cmdArg: Some_Generic_Type) -> AnyPublisher<String, Never> {
    guard (desiredCmd != nil) else {
        return Just("Nothing to execute\n").eraseToAnyPublisher()
    }
    desiredCmd?.send(cmdArg)
    return Just("Executed: \(String(describing: desiredCmd)) with argument: \(cmdArg)").eraseToAnyPublisher()
}

let cancellable = executeDesiredCmdSubj
    .flatMap(executeDesiredCmd)
    .receive(on: DispatchQueue.main)
    .sink(receiveValue: {
        print($0)
    })

desiredCmd = cmd1Subj
arg = 12345
desiredCmd?.send(arg!)
desiredCmd = cmd2Subj
arg = "This is the argument for command 2"
desiredCmd?.send(arg!)

where Some_Generic_Type is a placeholder that can be used to represent any type I attempt to pass through my PassthroughSubject. I tried using "Any" as the placeholder but it produces a couple of compilation errors:

  • "Cannot assign value of type 'PassthroughSubject<Int, Never>' to type 'PassthroughSubject<Any, Never>"
  • "Cannot assign value of type 'PassthroughSubject<String, Never>' to type 'PassthroughSubject<Any, Never>"

Solution

  • I was able to do this by creating a struct that defines a couple of enums with associated values:

    struct VersatilePassthroughSubject {
        enum SubjectOutputType {
            case subjAsStringOutput(PassthroughSubject<String, Never>)
            case subjAsIntOutput(PassthroughSubject<Int, Never>)
        }
        
        enum Arg {
            case argAsString(String)
            case argAsInt(Int)
        }
        
        var subj: SubjectOutputType? = nil
        var arg: Arg? = nil
    }
    
    let cmd1Subj = PassthroughSubject<Int, Never>()
    let cmd2Subj = PassthroughSubject<String, Never>()
    var desiredCmd: VersatilePassthroughSubject = VersatilePassthroughSubject()
    let executeDesiredCmdSubj = PassthroughSubject<Void, Never>()
    
    func executeDesiredCmd() -> AnyPublisher<String, Never> {
        guard (desiredCmd.subj != nil) else {
            return Just("Nothing to execute\n").eraseToAnyPublisher()
        }
        if case .subjAsIntOutput(let subj) = desiredCmd.subj {
            if case .argAsInt(let arg) = desiredCmd.arg {
                subj.send(arg)
                return Just("Executed: \(String(describing: desiredCmd)) with argument: \(arg)").eraseToAnyPublisher()
            }
        }
        else if case .subjAsStringOutput(let subj) = desiredCmd.subj {
            if case .argAsString(let arg) = desiredCmd.arg {
                subj.send(arg)
                return Just("Executed: \(String(describing: desiredCmd)) with argument: \(arg)").eraseToAnyPublisher()
            }
        }
        return Just("Nothing to execute\n").eraseToAnyPublisher()
    }
    
    let cancellable = executeDesiredCmdSubj
        .flatMap(executeDesiredCmd)
        .receive(on: DispatchQueue.main)
        .sink(receiveValue: {
            print($0)
        })
    
    desiredCmd.subj = .subjAsIntOutput(cmd1Subj)
    desiredCmd.arg = .argAsInt(12345)
    executeDesiredCmdSubj.send()
    desiredCmd.subj = .subjAsStringOutput(cmd2Subj)
    desiredCmd.arg = .argAsString("This is the argument for command 2")
    executeDesiredCmdSubj.send()