最近空き時間に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
の書き方に倣って書いてみています。
- 参考: Qiitaで自分が書いた記事です
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()
}
}
}
…おっと、flatMap と map が入り乱れていますね。少し解説してみます。
- 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のオペレーターである skip と zip を組み合わせると、実現することができます
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)
}
便利。
- 参考: Rxで1つ前の値を取得する