DelegateProxy の Observable にイベントが飛んでこないケースがあった


RxSwift (RxCocoa) には、標準ライブラリの主要な delegate メッセージに対応する Observable が用意されています。DelegateProxy という仕組みです。

今回、その中で UISearchControllerDelegate の各デリゲートメソッドを、RxSwift を用いて非同期的に扱いたかったのですが、ハマったのでまとめておきます。

うまくいかないパターン


class TheViewController: UIViewController, UISearchControllerDelegate {

    var searchController = UISearchController()
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        searchController.delegate = self

        // 1
        searchController.rx.willPresent.subscribe(onNext: { _ in
            /** これが呼ばれない **/
        }).disposed(by: disposeBag)
    }

    // 2
    func willPresentSearchController(_ searchController: UISearchController) {
        /** こっちは呼ばれる ***/
    }
}

上記の例では、UISearchControllerDelegate を、TheViewController 自身にセットして、willPresentSearchController を実装していますが、同時に RxSwift を使って willPresentSearchController の Observable 変数である willPresent に何らかの処理を非同期で行おうとしています。

こうすると、willPresent を subscribe しても実際にここにイベントが飛んでくることはありませんでした。

1 と 2 は同じタイミングで発火するものですが、delegate が viewController にセットされていることで 2 が優先されているといった状況になっているようです。

===
※ 実際には1つのクラス内でこんな妙な実装をすることはなく、プロトコルなどを使ってデリゲートの宣言や共通処理を切り出していたりするので、この問題のパターンになっていることに気付きませんでした。

うまくいくパターン


class TheViewController: UIViewController {

    var searchController = UISearchController()
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        // 1
        searchController.rx.willPresent.subscribe(onNext: { _ in
            /** 呼ばれる **/
        }).disposed(by: disposeBag)
    }
}

上記サンプルを見れば当たり前なのですが...

RxSwift の DelegateProxy で与えられる Observable を購読して非同期で処理したい場合は、それに集約する必要があります。つまり UIViewControllerDelegate を ViewController に指定して、デリゲートメソッドを実装する方法を同時に行わないということです。

親にあたるクラスやプロトコルでデリゲートが実装されていると、末端のクラスでこのように RxSwift の DelegateProxy 経由の Observable が飛んでこないことがあります。
ソースコードが大規模になってくるとプロトコルや継承によって見通しが悪くなると起こりやすいので、気を付ける必要がありそうです。