Search code examples
swiftbonjourrx-swift

RxSwift and Nested Subscriptions for Bonjour Discovery


I am very new to Rx and have been trying to make a Bonjour discovery client that resolves services. This is very simple to do imperatively, but I wanted to try with RxSwift.

Since the discovered NSNetService objects need to be persisted before resolution, I'm having to make a nested subscription call, the outer one for discovery, and the inner on for resolution...but something tells me this is not the best way.

import UIKit
import RxSwift

class BonjourClient: NSObject {

    let disposeBag = DisposeBag()
    var servicesArray = [NSNetService]()

    func startBrowsing() {
        let browser = NSNetServiceBrowser()
        browser.rx_netServiceBrowserDidFindServiceMoreComing
            .subscribeNext { (service: NSNetService) in
                        self.servicesArray.append(service)
                        self.servicesArray.last!.rx_netServiceDidResolveAddress
                            .subscribeNext { (sender: NSNetService) in
                                print("Resolved \(sender.name)")
                                let data = sender.TXTRecordData()
                                let dict: [String: NSData?] = NSNetService.dictionaryFromTXTRecordData(data!)
                                for (key, value) in dict {
                                    print("\(key) : \(String(data: value!, encoding: NSUTF8StringEncoding)!)")
                                }
                        }.addDisposableTo(self.disposeBag)
                        self.servicesArray.last!.resolveWithTimeout(5)
                }.addDisposableTo(disposeBag)
        browser.searchForServicesOfType("_amzn-wplay._tcp.", inDomain: "local.")
        NSRunLoop.currentRunLoop().run()
    }

}

My proxy classes are as follows:

import UIKit
import RxSwift
import RxCocoa

class RxNSNetServiceBrowserDelegateProxy: DelegateProxy, NSNetServiceBrowserDelegate, DelegateProxyType {

    static func currentDelegateFor(object: AnyObject) -> AnyObject? {
        let browser: NSNetServiceBrowser = object as! NSNetServiceBrowser
        return browser.delegate
    }

    static func setCurrentDelegate(delegate: AnyObject?, toObject object: AnyObject) {
        let browser: NSNetServiceBrowser = object as! NSNetServiceBrowser
        browser.delegate = delegate as? NSNetServiceBrowserDelegate
    }

}

class RxNSNetServiceDelegateProxy: DelegateProxy, NSNetServiceDelegate, DelegateProxyType {

    static func currentDelegateFor(object: AnyObject) -> AnyObject? {
        let service: NSNetService = object as! NSNetService
        return service.delegate
    }

    static func setCurrentDelegate(delegate: AnyObject?, toObject object: AnyObject) {
        let service: NSNetService = object as! NSNetService
        service.delegate = delegate as? NSNetServiceDelegate
    }

}

extension NSNetServiceBrowser {

    public var rx_delegate: DelegateProxy {
        return proxyForObject(RxNSNetServiceBrowserDelegateProxy.self, self)
    }

    public var rx_netServiceBrowserDidFindServiceMoreComing: Observable<NSNetService> {
        return rx_delegate.observe("netServiceBrowser:didFindService:moreComing:")
            .map { params in
                let service = params[1] as! NSNetService
                return service
        }
    }

}

extension NSNetService {

    public var rx_delegate: DelegateProxy {
        return proxyForObject(RxNSNetServiceDelegateProxy.self, self)
    }

    public var rx_netServiceDidResolveAddress: Observable<NSNetService> {
        return rx_delegate.observe("netServiceDidResolveAddress:")
            .map { params in
                return params[0] as! NSNetService

        }
    }
}

If I use flatMap after the browser.rx_netServiceBrowserDidFindServiceMoreComing call instead of subscribeNext, the service won't resolve because I can't persist it to an array from within flatMap for reasons that escape me, mostly from never having touched Rx. Am I bound to using nested calls?

Short version of my problem is the above works, but seems convoluted to me. Any ideas would be greatly appreciated.


Solution

  • You can use scan to avoid nested subscription. It will add each NSNetService from rx_netServiceBrowserDidFindServiceMoreComing to the array. Note that you don't event have to store servicesArray as a member variable in this case, unless you need it for another reason.

    Then you can use flatMap as follows:

    browser.rx_netServiceBrowserDidFindServiceMoreComing
        .scan([NSNetService]()) { (services: [NSNetService], service: NSNetService)  in
            return services + [service]
        }.flatMap { (services: [NSNetService]) in
            return services.last!.rx_resolveWithTimeout(5)
        }.subscribeNext { (sender: NSNetService) in
            print("Resolved \(sender.name)")
            let data = sender.TXTRecordData()
            let dict: [String: NSData?] = NSNetService.dictionaryFromTXTRecordData(data!)
            for (key, value) in dict {
                print("\(key) : \(String(data: value!, encoding: NSUTF8StringEncoding)!)")
            }
    }.addDisposableTo(disposeBag)
    

    This requires adding a method in NSNetService extension, as you have to return an Observable from flatMap:

    extension NSNetService {
    //existing methods omitted
     public func rx_resolveWithTimeout(timeout: NSTimeInterval) -> Observable<NSNetService> {
            self.resolveWithTimeout(timeout)
            return rx_netServiceDidResolveAddress.filter {
                $0 == self
            }
        }
    }