DeclarativeHub / ReactiveKit

A Swift Reactive Programming Kit
MIT License
1.24k stars 115 forks source link

ReactiveKit + Combine, combined? #251

Open kevinrenskers opened 4 years ago

kevinrenskers commented 4 years ago

I have a UIKit app, where I am starting to integrate a bit of SwiftUI into (via UIHostingController). My SwiftUI views need to use Combine (@State, or @ObservedObject+@Published for example), but I'd still like these views to work with my existing ReactiveKit Signals. Is this possible without writing a bunch of boilerplate code? Has anyone else tried to make ReactiveKit and Combine work together in one app?

srdanrasic commented 4 years ago

Hey @kevinrenskers, check out the latest release. I've added toPublisher() extension method to SignalProtocol that converts ReactiveKit signal into Combine publisher, as well as toSignal() extension on Publisher that converts it the other way around.

kevinrenskers commented 4 years ago

Very cool! 🎉

I love ReactiveKit's easy way to wrap old-fashioned async code with callbacks into Signals (https://github.com/DeclarativeHub/ReactiveKit#wrapping-asynchronous-calls-into-signals). I found it quite hard to do the same in pure Combine, so these new extensions will really help to go from async calls to Combine publishers. But, let's say I would want to skip the ReactiveKit in-between step at some point, would you suggest a similar route as the code in https://github.com/DeclarativeHub/ReactiveKit/blob/master/Sources/Combine.swift#L51? With the nested struct etc?

srdanrasic commented 4 years ago

@kevinrenskers yeah, I miss that in Combine too. One approach could be to implement Signal in Combine, it should be pretty easy. Here is my quick attempt:

public struct Signal<Output, Failure: Error>: Combine.Publisher {

    private class Subscription: Combine.Subscription {

        let producer: (AnySubscriber<Output, Failure>) -> Combine.AnyCancellable
        let subscriber: AnySubscriber<Output, Failure>

        var cancellable: Combine.AnyCancellable?

        init(producer: @escaping (AnySubscriber<Output, Failure>) -> Combine.AnyCancellable, subscriber: AnySubscriber<Output, Failure>) {
            self.producer = producer
            self.subscriber = subscriber
        }

        func request(_ demand: Combine.Subscribers.Demand) {
            cancellable = producer(subscriber)
        }

        func cancel() {
            cancellable?.cancel()
        }
    }

    private let producer: (AnySubscriber<Output, Failure>) -> Combine.AnyCancellable

    public init(_ producer: @escaping (AnySubscriber<Output, Failure>) -> Combine.AnyCancellable) {
        self.producer = producer
    }

    public func receive<S>(subscriber: S) where S: Combine.Subscriber, Failure == S.Failure, Output == S.Input {
        let subscription = Subscription(producer: producer, subscriber: AnySubscriber(subscriber))
        subscriber.receive(subscription: subscription)
    }
}
let signal = Signal<Int, TestError> { subscriber in
    subscriber.receive(1)
    subscriber.receive(2)
    subscriber.receive(completion: .finished)
    return Combine.AnyCancellable {
        print("Cancelled")
    }
}

signal.sink(receiveCompletion: { print($0) }, receiveValue: { print($0) })