UITabBarControllerのselectedIndexをObserveする

Tuesday, December 27, 2016

最近空き時間にRxを触ってあれこれ書いていたりします。

UITabBarController のTabBarItemがタップされたのをObserveしたかったのですが、
RxSwift(RxCocoa)でそのまま提供されていないので extension として作成してみました。

KVOをObservableとして受け取るのを利用する

今回はこちらの関数を使用します。

public func observe<E>(_ type: E.Type, _ keyPath: String, options: NSKeyValueObservingOptions = default, retainSelf: Bool = default) -> RxSwift.Observable<E?>
public func observeWeakly<E>(type: E.Type, _ keyPath: String, options: NSKeyValueObservingOptions) -> Observable<E?>

これを使うことで、KVOで監視し、変更があった時にObservableとして流すことができます。
RxSwiftの例にもありますが、こんな形で、受け取る型とkeypathを指定します。

let view: UIView = ...

view
  .rx.observe(CGRect.self, "frame")
  .subscribe(onNext: { frame in
    ...
  })

ただし、ちょっとした注意点が。

  • NSKeyValueObservingOptions.new .initial がデフォルトで指定されています。
  • retainSelf はtrueがデフォルトで指定されています
  • observe でretainSelf=trueだと、レシーバーを強参照してしまうので、注意が必要です
    retainSelf=false または、 observeWeakly を使うほうがベターかもしれません。

ということで、寄り道が過ぎましたが、以下のように UITabBarControllerの selectedIndex をObserveしてみましょう。

// 簡略化のため "!" を使っています
self.tabBarController!
    .observeWeakly(UIViewController.self, "selectedViewController")
    .subscribe({ print($0) })
    .addDisposableTo(disposeBag)

しかし…

これでタブをタップしてみると、、なんと流れてきません。

一見、 selectedIndex をKVOして値を監視すれば良いようにも見えますが…
実は selectedIndex をObserveすることができません。
(単純に selectedIndexがgetterだからなのかもしれません)

ただ、ここで諦めてはいけません。 selectedViewController が残っています。 こやつはKVOで値の変更を監視することが可能のようです。

selectedViewController を利用して作ってみた結果

作ってみた結果、以下のようになりました。
RxSwiftの extension の書き方に倣って書いてみています。

extension Reactive where Base: UITabBarController {
    public var selectedIndex: Observable<Int> {
        return self.observeWeakly(UIViewController.self, "selectedViewController")
            .flatMap { $0.map { Observable.just($0) } ?? Observable.empty()  }
            .flatMap { [weak base] in
                return base?.viewControllers?.index(of: $0).map { Observable.just($0) } ?? Observable.empty()
        }
    }
}

…おっと、flatMapmap が入り乱れていますね。少し解説してみます。

  • KVOで値を監視して、変更があったらストリームを流す

まず1行目では、observeWeakly を用いて、 selectedViewController を監視します。
もし selectedViewController が変更された場合はストリームが流れて最初の flatMap に到達します。

  • 最初の flatMap

ちなみに同じタブをタップした場合でもしっかりストリームが流れてきてくれます。 最初の flatMap は、先で受け取ったKVOの結果である Observable<UIViewController?>Observable<UIViewController> に変換しています。
その flatMap のclosure内の map は、Rxの map ではなく、Optional型に対しての map です。もし値がnilだったら emptyを流すようにします。
このnilをフィルタリングする方法、 役に立ちます。

  • 最後の flatMap

ここでは、 base(=UITabBarController)viewControllers から流れてきた値(=viewController)が何番目にあるのかを index(of:) で探って、
indexがあれば Observable<Int>として変換して流します。
extension Reactive where Base: UITabBarController と、Baseを UITabBarControllerとして束縛してあげることで、 base がUITabBarControllerのインスタンスであると定まるので、これを利用します。
ただし、解放されている可能性もあるので、 [weak base] で弱参照してあげます。

ここまでを踏まえて

改めて最初に出したサンプルを以下のように書き換えて実行すると、タップされたタブのindexが流れてくるようになります 👌

if let tabBarController = self.tabBarController {
    tabBarController.rx
        .selectedIndex
        .subscribe({ print($0) })
    .addDisposableTo(disposeBag)
}

応用 : 2回同じタブがタップされたのを検知する

Rxのオペレーターである skipzip を組み合わせると、実現することができます

if let tabBarController = self.tabBarController {
    let selectedIndex = tabBarController.rx.selectedIndex
    Observable
        .zip(selectedIndex, selectedIndex.skip(1)) { $0 == $1 }
        .filter { $0 }
        .subscribe { _ in print("tapped twice!!") }
    .addDisposableTo(disposeBag)
}

便利。

techSwiftRxSwift

2016年の振り返り

システム標準の青いtintColor