RxSwift使いこなせてますか?
僕もまだ十分とは言えないですが、半年以上はプロダクトに使用してきたので、レビューで指摘されたことを中心に細々としたtipsをまとめてみようと思います
全体的な勉強方針は前回の記事にまとめたので、こちらも良ければ見てください
RxとMVVMの勉強ガイド(自分がやってきたこと)
クロージャの使用を避ける
subscribe(onNext:)
などはクロージャに任意のコードが書けるので非常に便利なのですが、やりすぎると手続き型的なコードになりがちだったり、selfを適切に弱参照でキャプチャしないといけなかったりするので、他に代替手段がない場合のみ使用するのが良いと思います。
以下に自分が指摘されたことのある代替手段を示します。
オプショナルをアンラップしたい
someObservable // Int?が流れてくるとする
.subscribe(onNext: { [weak self] optionalInt in
guard let someInt = optionalInt else {
return
}
self?.someFunc(someInt) // Intを1つ引数に取る関数とする
})
.disposed(by: disposeBag)
こちらは、flatMap()
を使って書き換えることができます。
someObservable // Int?が流れてくるとする
.flatMap { $0.flatMap(Observable.just) ?? Observable.empty() } // ここでアンラップ
.subscribe(onNext: { [weak self] someInt in // Intが流れてきた!
self?.someFunc(someInt)
})
.disposed(by: disposeBag)
参考:
Observableをnilでフィルタしてアンラップする
関数を呼び出したい
上のコードの続きで、subscribe(onNext:)
の中身はsomeFunc()
実行するだけなので、クロージャではなく関数のシグネチャを渡すだけに書き換えられます。
someObservable // Int?が流れてくるとする
.flatMap { $0.flatMap(Observable.just) ?? Observable.empty() } // ここでアンラップ
.subscribe(onNext: someFunc) // someFuncを呼び出す、流れてきたIntが引数として渡される
.disposed(by: disposeBag)
コメントにて指摘をいただきました。
上の形だと、selfを強参照で持ってしまうようなので、循環参照を避けるためにはキャプチャを明示的に書いてあげないといけないようです!
よって、代替コード(オプショナルのアンラップ)の形で書くのが良いかと思います!
エラーが流れてこないのであればbind(onNext:)
を使うこともできます。万が一エラーを流してしまうとクラッシュするので気をつけてください!
参考:
rxswift bind(onNext: VS subscribe(onNext:
複数のストリームを一つにまとめる
merge()
を使用することで複数のストリームを一つにまとめることができます。
結果が同じになる複数の操作をまとめて書くことができます。
検索履歴の操作を例にコードを示します。操作には追加、全削除、削除の3つがあり、全て操作後の検索履歴の配列を返します。その配列をDBなどのデータストアに保存したあと、画面を更新します。
input.didTapSearch // didTapSearch: PublishRelay<String>
.map { history.add(word: $0) } // func add(word: String) -> [String]
.do(onNext: dataStoreManager.saveHistory) // func saveHistory(list: [String])
.bind(to: output.history) // history: PublishRelay<[String]>
.disposed(by: disposeBag)
input.didTapAllDelete // didTapAllDelete: PublishRelay<Void>
.map { history.clear() } // func clear() -> [String]
.do(onNext: dataStoreManager.saveHistory) // func saveHistory(list: [String])
.bind(to: output.history) // history: PublishRelay<[String]>
.disposed(by: disposeBag)
input.didTapDelete // didTapDelete: PublishRelay<String>
.map { history.delete(word: $0) } // func delete(word: String) -> [String]
.do(onNext: dataStoreManager.saveHistory) // func saveHistory(list: [String])
.bind(to: output.history) // history: PublishRelay<[String]>
.disposed(by: disposeBag)
重複しているコードが多いのでまとめられそうな感じがします。
merge()を使うと以下のようになります。
Observable.merge(
input.didTapSearch.map { history.add(word: $0) }, // Observable<[String]>
input.didTapAllDelete.map { history.clear() }, // Observable<[String]>
input.didTapDelete.map { history.delete(word: $0) } // Observable<String>
)
.do(onNext: dataStoreManager.saveHistory)
.bind(to: output.history)
.disposed(by: disposeBag)
merge()
でまとめる場合、当然ながら型は一致している必要があります。
型が同じでも、違う概念のものはまとめないほうが良いです。その場合は後続の処理が一致してないのでまとめられないと思いますが。
例のコードではすべて検索履歴に対する処理だったのでうまくまとめることができました。
カスタムビューにrxを生やす
ReactiveのExtensionを実装すると、標準ビューのように.rx
を生やすことができます。
Rxに関連あるプロパティであることを明示できるので、ControlEvent
やBinder
で使うと良いと思います。
class SomeCustomView: UIView {
let someStatus: BehaviorRelay<Bool>
let someAction: PublishRelay<Void>
...
}
let customView = CustomView()
customView.someStatus.subscribe(onNext: {...
customView.someStatus.accept(...)
customView.someAction.subscribe(onNext: {...
クラスのプロパティを公開することでも、外から状態やイベントを監視することもできますが、ReactiveのExtensionを使ったほうがわかりやすくなると思います。Binder
の場合はイベントが流れてきた処理も一緒に書けるのでオススメです。
extension Reactive where Base: CustomView {
var someStatus: Binder<Bool> {
return Binder(base) { view, value in
// 状態が更新されたときの処理
}
}
var someAction: ControlEvent<Void> {
return .init(events: /* 外に伝えたいイベント */)
// UIButton.rx.tapなどもともとControlEventのものはそのまま返せばOK
// return base.someButton.rx.tap 例えばこんな感じ
}
}
let customView = CustomView()
someBool.bind(to: customView.rx.someStatus)...
customView.rx.someAction.subscribe(onNext: {...
Rxを使っている感が高まりました!
フラグによる状態管理を無くす
特定の状態になるまではイベントを無視したい、あるいは画面が表示されたときに1度だけ処理したい、などという要求は度々発生します。
そんなときにはskip()
とtake()
というオペレータを使用することができます。
最初のn回のイベントを無視する
skip()
を使うことで、引数で与えた回数分だけイベントを無視することができます。
個人的にはBehaviourRelay
と組み合わせて使うことが多いです。
APIの結果をBehaviourRelay
に入れたいが、BehaviourRelay
には初期値が必要なのと、subscribe()
したときにその時保持している値が流れてしまうので、初期値を無視するために使用します。
var isLoaded: Bool = false // APIが完了したらtrueになる
let apiResult = BehaviorRelay<[String]>(value: []) // APIの結果が入る、初期値はいらない
apiResult
.subscribe(onNext: { result in
if isLoaded { // APIが完了している場合のみ処理したい
// 何かしらの処理
}
})
.disposed(by: disposeBag)
skip()
を使うことでフラグを消すことができます。
let apiResult = BehaviorRelay<[String]>(value: []) // APIの結果が入る、初期値はいらない
apiResult
.skip(1) // 最初の1回(初期値)は無視する
.subscribe(onNext: { result in
// 何かしらの処理
})
.disposed(by: disposeBag)
最初のn回以降イベントを無視する
take()
を使うことで、引数で与えた回数分以降のイベントを無視することができます。
個人的には、画面が表示されたときに1回だけ処理を行いたい(けどAutoLayoutの関係でviewDidLoad()
には書けない)時に使用することが多いです。
var isInitial: Bool = true // 1回だけ実行したい処理を終えたらfalseになる
let viewDidAppear = PublishRelay<Void>() // viewDidAppearのタイミングで発火されるイベント
viewDidAppear
.subscribe(onNext: { result in
if isInitial { // 最初の1回だけ
// 何かしらの処理
isInitial = false // フラグ下ろす
}
})
.disposed(by: disposeBag)
take()
を使ってフラグを消します。
let viewDidAppear = PublishRelay<Void>() // viewDidAppearのタイミングで発火されるイベント
viewDidAppear
.take(1) // 最初の1回以降は無視
.subscribe(onNext: { result in
// 何かしらの処理
})
.disposed(by: disposeBag)
エラーが出たときは型を注意深く確認する
最後は開発中の話ですが、Rx関連でコンパイラが出すエラーは当てにならないことが多いです。
なにかエラーが出たときは、ストリームを流れるイベントの型を注意深く追ってみましょう。
おわりに
自分の経験を元に注意すべき点やこうすればもっと良くなるという点をまとめてみました
まだまだ自分も勉強中なので、もっと気をつける点などあると思います
是非そういったところや経験談などをコメントいただけると嬉しいです!