iOS
Swift
WatchKit
RxSwift
WatchConnectvity

WCSessionのReactive Extensionを作ってみた

More than 1 year has passed since last update.

Web APIのReactive Extensionは I/Oが、1:1なので、作れたのですが、Hoge / HogeDelagateのような関係のReactive Extensionってどう作るのだろう?と思ったので、WCSessionをお題に作ってみました。(watchOSの方のWCSessionです)とは言っても、RxSwiftからサンプルとしてCLLocationManagerが先行してコード公開されているので、こちらを参考にさせていただきました。写経です。
Github - RxExample/Extensions
同じような方法で、Hoge / HogeDelagateのような関係のクラスの Reactive Extension を作れるので、ノウハウ共有したいと思います。

はじめに

このWCSessionのコードの完全版は、Github & CocoaPods の練習がてら公開してみました。そう、なぜかFrameworkとして、CocoaPodsから追加できるのです。
Github - WCSessionRx
なお、Swift ライブラリの公開の方法はこちらの記事を大変参考にさせていただきました。非常に助かりました。ありがとうございます。
Qiita - Swiftでライブラリを公開する

つくりかた

最終的には、
1. WCSession+Rx.swift
2. RxWCSessionDelegateProxy.swift
の2つで構成されます。
これが実装されると、こんな具合で、WCSessionのDelegateイベントを受け取れるようになります。

disposer = WCSession.default()
            .rx.didReceiveMessage
            .subscribe(onNext: {
                self.someLabel.setText($0)
            }, onError: {
                self.someLabel.setText("Error !! \($0.localizedDescription)")
            }, onCompleted: {
                self.someLabel.setText("Completed.")
            }) {
                self.someLabel.setText("Canceled.")
        }

WCSessionDelegateのプロキシーを作る

先ずは、Proxyクラスを作ります。

RxWCSessionDelegateProxy.swift
class RxWCSessionDelegateProxy: DelegateProxy, WCSessionDelegate, DelegateProxyType {

    class func currentDelegateFor(_ object: AnyObject) -> AnyObject? {
        let session: WCSession = object as! WCSession
        return session.delegate
    }

    class func setCurrentDelegate(_ delegate: AnyObject?, toObject object: AnyObject) {
        let session: WCSession = object as! WCSession
        session.delegate = delegate as? WCSessionDelegate
    }
}

それを呼び出せるようにWCSession+Rx側でプロパティにします。

WCSession+Rx.swift
extension Reactive where Base: WCSession {
    public var delegate: DelegateProxy {
        return RxWCSessionDelegateProxy.proxyForObject(base)
    }
}

下準備は、以上です。次にDelegateメソッドをストリームにしていきます。

OptionalなDelegateメソッドをストリームにする

セレクターを使って、変換します。Observable<?>を何にするかは、Delegateメソッドのパラメータを見て、有用そうなものを選びます。sessionReachabilityDidChangeは、パラメータにWCSessionしか渡されないのですが、Reachabilityが変化した時のイベントですので、WCSession#isReachableを返すストリームにします。

WCSession+Rx.swift
    // WCSessionDelegate
    // @available(watchOS 2.0, *)
    // optional public func sessionReachabilityDidChange(_ session: WCSession)
    // の、ストリーム
    public var didChangeReachability: Observable<Bool> {
        return delegate.methodInvoked(#selector(WCSessionDelegate.sessionReachabilityDidChange(_:))).map{ a in
            return try self.castOrThrow(WCSession.self, a[0]).isReachable
        }
    }
    // TODO to Utility...
    fileprivate func castOrThrow<T>(_ resultType: T.Type, _ object: Any) throws -> T {
        guard let returnValue = object as? T else {
            throw RxCocoaError.castingError(object: object, targetType: resultType)
        }

        return returnValue
    }

この時、a というパラメータが、そのまま、Delegateメソッドのパラメータの配列になるので、a[0]は0番目の引数... つまり、WCSessionが入ってきます。

methdInvokedについて
    // こんなDelegateメソッドがあったら、methdInvokedを通すことで、
    // a[0] = name
    // a[1] = age
    // になって返ってきます。
    func delegateFunction(name:String, age:Int) -> Void {
    }

Delegateを使っていると、Selectorにするときに、同一ラベルのメソッドはビルドエラーになってしまうのですが、これに関しては、こちらの記事で解決方法を書いています。

RequireなDelegateメソッドをストリームにする

WCSessionDelegateだと、以下のメソッドが実装必須となります。

WCSessionDelegate
/** Called when the session has completed activation. If session state is WCSessionActivationStateNotActivated there will be an error with more details. */
    @available(watchOS 2.2, *)
    public func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?)

上記からわかるように、WCSessionActivationStateの変更を検知するイベントです。なので、WCSessionActivationStateのストリームを作ります。が、必須メソッドを先ほどのような実装方法でストリームにすると、うまく動作しません。なぜかというと、RxWCSessionDelegateProxy側で実装されてしまっているからです。(WarningがConsole上からも確認できます)

RxWCSessionDelegateProxy.swift
class RxWCSessionDelegateProxy: DelegateProxy, WCSessionDelegate, DelegateProxyType {

    ~ 中略 ~

    // 必須メソッドなので実装されている
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
    }
}

なので、ストリーム化をProxy側で、実装します。

RxWCSessionDelegateProxy.swift
class RxWCSessionDelegateProxy: DelegateProxy, WCSessionDelegate, DelegateProxyType {

    ~ 中略 ~

    fileprivate var _activationStateSubject: PublishSubject<WCSessionActivationState>? // 監視対象のサブジェクトを作り...

    // 遅延初期化
    internal var activationStateSubject: PublishSubject<WCSessionActivationState> {
        if let subject = _activationStateSubject {
            return subject
        }

        let subject = PublishSubject<WCSessionActivationState>()
        _activationStateSubject = subject

        return subject
    }

    // WCSessionActivationStateの変更検知のイベントで、発火
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {

        if let error = error {
            _activationStateSubject?.on(.error(error))
        } else {
            _activationStateSubject?.on(.next(activationState))
        }
        // 本来のデリゲート側にイベントをリレーする
        _forwardToDelegate?.session(session, activationDidCompleteWith: activationState, error: error)
    }

    deinit {
        // 削除時に completed
        _activationStateSubject?.on(.completed)
    }
}

どこでCompleteするかは、その監視対象によりけりだと思います。監視をやめたタイミングなど、色々あると思います。が、とりあえずインスタンスが破棄されたタイミングにしています。
この調子で、メソッドを作っていけば、終わりです。

以上です。コード全体はこちらを参照ください。
ありがとうございました。