タコさんブログ

プログラミングメモと小言

デリゲートメソッドをRxSwift対応させる方法

この例ではUIWebViewDelegateメソッドをRxSwift対応させる。

環境

準備

RxCocoa、RxSwiftをインポート。

import RxCocoa
import RxSwift

今のところ以下のキャスト時に使用する関数はパブリックになっていないので、RxCocoaからコピーしてくる。

@noreturn func rxFatalError(lastMessage: String) {
  fatalError(lastMessage)
}

func castOptionalOrFatalError<T>(value: AnyObject?) -> T? {
  if value == nil {
    return nil
  }
  let v: T = castOrFatalError(value)
  return v
}

func castOrThrow<T>(resultType: T.Type, _ object: AnyObject) throws -> T {
  guard let returnValue = object as? T else {
    throw RxCocoaError.CastingError(object: object, targetType: resultType)
  }
  return returnValue
}

func castOptionalOrThrow<T>(resultType: T.Type, _ object: AnyObject) throws -> T? {
  if NSNull().isEqual(object) {
    return nil
  }
  guard let returnValue = object as? T else {
    throw RxCocoaError.CastingError(object: object, targetType: resultType)
  }
  return returnValue
}

func castOrFatalError<T>(value: AnyObject!, message: String) -> T {
  let maybeResult: T? = value as? T
  guard let result = maybeResult else {
    rxFatalError(message)
  }
  return result
}

func castOrFatalError<T>(value: Any!) -> T {
  let maybeResult: T? = value as? T
  guard let result = maybeResult else {
    rxFatalError("Failure converting from \(value) to \(T.self)")
  }
  return result
}

DelegateProxyクラスを作成

対応させるデリゲート(UIWebViewDelegate)のプロキシークラスを作成する。

class RxUIWebViewDelegateProxy: DelegateProxy, UIWebViewDelegate, DelegateProxyType {

  class func currentDelegateFor(object: AnyObject) -> AnyObject? {
    let webView: UIWebView = castOrFatalError(object)
    return webView.delegate
  }

  class func setCurrentDelegate(delegate: AnyObject?, toObject object: AnyObject) {
    let webView: UIWebView = castOrFatalError(object)
    webView.delegate = castOptionalOrFatalError(delegate)
  }
}

Reactive ラッパー

UIWebViewのエクステンションとして、Reactiveラッパーを用意する。今回は以下のUIWebViewDelegateメソッドをラップする。

  • webViewDidStartLoad(webView: UIWebView)
  • webViewDidFinishLoad(webView: UIWebView)
  • webView(webView: UIWebView, didFailLoadWithError error: NSError?)
extension UIWebView {
  //  Reactive wrapper for `delegate`.
  public var rx_delegate: DelegateProxy {
    return proxyForObject(RxUIWebViewDelegateProxy.self, self)
  }
  //  Reactive wrapper for webViewDidStartLoad(_:)
  public var rx_webViewDidStartLoad: Observable<UIWebView> {
    let sel = #selector(UIWebViewDelegate.webViewDidStartLoad(_:))
    let o = rx_delegate
      .observe(sel)
      .map { a in
        return try castOrThrow(UIWebView.self, a[0])
    }
    return o
  }
  //  Reactive wrapper for webViewDidFinishLoad(_:)
  public var rx_webViewDidFinishLoad: Observable<UIWebView> {
    let sel = #selector(UIWebViewDelegate.webViewDidFinishLoad(_:))
    let o = rx_delegate
      .observe(sel)
      .map { a in
        return try castOrThrow(UIWebView.self, a[0])
    }
    return o
  }
  //  Reactive wrapper for webView(_:didFailLoadWithError:)
  public var rx_webViewDidFailLoadError: Observable<(UIWebView, NSError?)> {
    let sel = #selector(UIWebViewDelegate.webView(_:didFailLoadWithError:))
    let o = rx_delegate
      .observe(sel)
      .map { a -> (UIWebView, NSError?) in
        let webview = try castOrThrow(UIWebView.self, a[0])
        let error = try castOptionalOrThrow(NSError.self, a[1])
        return (webview, error)
    }
    return o
  }
}

戻り値のあるデリゲートメソッドの場合

DelegateProxy.setForwardToDelegate(_:retainDelegate:) を使用して通常と同じように扱う。

使用例

class ExampleViewController: UIViewController, UIWebViewDelegate {
  @IBOutlet weak var webView: UIWebView!
  let disposeBag = DisposeBag()

  override func viewDidLoad() {
    webView
      .rx_webViewDidStartLoad
      .subscribeNext { webview in
        print("webViewDidStartLoad", webview)
      }.addDisposableTo(disposeBag)

    webView
      .rx_webViewDidFinishLoad
      .subscribeNext { webview in
        print("webViewDidFinishLoad", webview)
      }.addDisposableTo(disposeBag)

    webView
      .rx_webViewDidFailLoadError
      .subscribeNext { (webview, error) in
        print("webViewDidFailLoadError", webview, error)
      }.addDisposableTo(disposeBag)

    webView.rx_delegate.setForwardToDelegate(self, retainDelegate: false)

    let url = NSURL(string: "https://xxx")!
    let request = NSURLRequest(URL:url)
    webView.loadRequest(request)
  }

  func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
    print(request.URL?.absoluteString,)
    return true
  }
}

参考URL