Reputation: 4912
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://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
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