AJ Venturella
AJ Venturella

Reputation: 4912

How to properly convert a 3rd party library delegate into a RxSwift Observable

I have a case where I am using a 3rd party library and I would like to make it into an Observable. Appropriately, the library is designed around delegates as one would expect so I am wrapping it. The library performs an async operation and calls it's delegate with the results when it completes.

I definitely want to take advantage of the cold nature of the observable and only start the operation when someone subscribes. I have a solution that works, I just don't know if it's deeply flawed and I am missing some important understanding of RxSwift or perhaps there is a simpler way to achieve the same goal.

public final class RxLibBridge: LibDelegate{

    let lib = Lib()
    let _source = PublishSubject<[LibResult]>()

    public init(){
        lib.delegate = self
    }

    public func asObservable() -> Observable<[LibResult]>{
        // create a cold observable to start
        // the Lib's async operation on subscribe.
        return Observable<Void>.create{
            observer in

            self.lib.startOperation()

            // emit and complete
            observer.onNext(())
            observer.onCompleted()
            return Disposables.create()
        }
        // convert the `Void` observable into an observable from the 
        // PublishSubject
        .flatMapLatest{self._source}
    }

    // the lib's completion delegate method
    public func lib(_ lib: Lib, didFinishWithResult results: [LibResult]) {
        // grab the PublishSubject, emit the result and complete
        let observer = _source.asObserver()
        observer.onNext(results)
        observer.onCompleted()
    }
}

So my question is: Is this an appropriate pattern in Rx? Again, it works:

RxLibBridge()
    .asObservable()
    .subscribe(...)

Just because it works though doesn't mean I have not fundamentally misunderstood the proper way to work with this situation.

I know there is a way in RxSwift to handle something like this:

https://medium.com/@maxofeden/rxswift-migrate-delegates-to-beautiful-observables-3e606a863048#.rksg2ckpj

https://samritchie.net/2016/05/12/rxswift-delegateproxy-with-required-methods/

I tried this approach but it looks like the API changed since 2015. Namely, in the example links above proxyForObject cannot be found when adding the rx_delegate method in the extension.

Additionally, this approach appears to favor pure Objective-C [UIKit/AppKit] APIs. In my attempt to follow the linked example, I was editing the source of the 3rd party lib to make the delegate method optional and exposing it to @objc. The lib's delegate is required and I would rather not have to fork the lib to make the modifications.

This SO answer provided the updated API for the 2 links above:

Can not use proxyForObject function in DelegateProxyType (rxSwift)

Upvotes: 4

Views: 1930

Answers (1)

AJ Venturella
AJ Venturella

Reputation: 4912

So after digging some more, it looks like this will do the trick with a required delegate method, updated for RxSwift 3.3.1. This is using their DelegateProxy system.

import RxSwift
import RxCocoa
import Lib


public final class RxLibDelegate: DelegateProxy, LibDelegate, DelegateProxyType{

    let _subject = PublishSubject<[LibResult]>()

    public static func currentDelegateFor(_ object: AnyObject) -> AnyObject?{
        let target = object as! Lib
        return target.delegate
    }

    public static func setCurrentDelegate(_ delegate: AnyObject?, toObject object: AnyObject) {
        let target = object as! Lib
        target.delegate = delegate as? LibDelegate
    }

    public func lib(_ lib: Lib, didFinishWithResult results: [LibResult]) {
        _subject.onNext(results)
        _subject.onCompleted()
    }
}



extension Lib{

    public var rx_delegate: DelegateProxy{
        // `proxyForDelegate` moved as compared to examples at:
        // https://samritchie.net/2016/05/12/rxswift-delegateproxy-with-required-methods/
        // https://medium.com/@maxofeden/rxswift-migrate-delegates-to-beautiful-observables-3e606a863048#.rksg2ckpj

        return RxLibDelegate.proxyForObject(self)
    }

    public var rx_libResults: Observable<[LibResult]> {
        // `proxyForDelegate` moved as compared to examples at:
        // https://samritchie.net/2016/05/12/rxswift-delegateproxy-with-required-methods/
        // https://medium.com/@maxofeden/rxswift-migrate-delegates-to-beautiful-observables-3e606a863048#.rksg2ckpj

        let proxy = RxLibDelegate.proxyForObject(self)
        return proxy._subject
    }
}

That's about 28 LOC. My original "wrapper" (see updated version below) but I don't know if it's the best is 21 LOC; 6 of 1 half dozen of the other?

In my particular case I only have 1 delegate method to worry about. If you were working with some functionality that had multiple delegates I think the DelegateProxy + extension methods would be a lot more practical and the better choice in that case.

Regarding my original trial wrapping thing using that Void observable, it appears it's totally acceptable to alter the stream with flatMapLatest as evidenced here re: Sending continual events while a button is pressed:

https://stackoverflow.com/a/39123102/1060314

import RxSwift
import RxCocoa


let button = submitButton.rx_controlEvent([.TouchDown])
button
.flatMapLatest { _ in
    Observable<Int64>.interval(0.1, scheduler: MainScheduler.instance)
        .takeUntil(self.submitButton.rx_controlEvent([.TouchUpInside]))
}
.subscribeNext{ x in print("BOOM \(x)") }
.addDisposableTo(disposeBag)

//prints BOOM 0 BOOM 1 BOOM 2 BOOM 3 BOOM 4 BOOM 5 for every 0.1 seconds

Note that a new Observable is returned from flatMapLatest. The author cites the RxSwift slack channel, so I assume it is at least acceptable to do.

Here's an updated version of my wrapper version that I think might be a bit cleaner:

import RxSwift


public final class RxLibBridge: LibDelegate{

    let lib = Lib()
    let _source = PublishSubject<[LibResult]>()

    public init(){
        lib.delegate = self
    }

    public func asObservable() -> Observable<[LibResult]>{
        // create a cold observable to start
        // the Lib's async operation on subscribe.
        return Observable.just(())
            .do(onNext: {
                self.lib.startOperation()
            })
            .flatMapLatest{self._source}
    }

    // the lib's completion delegate method
    public func lib(_ lib: Lib, didFinishWithResult results: [LibResult]) {
        // grab the PublishSubject, emit the result and complete
        _source.onNext(results)
        _source.onCompleted()
    }
}

Upvotes: 4

Related Questions