39
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

and factoryAdvent Calendar 2019

Day 2

脱RxSwift初心者のためのtips

Last updated at Posted at 2019-12-02

RxSwift使いこなせてますか?:smiley_cat:
僕もまだ十分とは言えないですが、半年以上はプロダクトに使用してきたので、レビューで指摘されたことを中心に細々としたtipsをまとめてみようと思います:bulb:
全体的な勉強方針は前回の記事にまとめたので、こちらも良ければ見てください:eyes:
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)

:warning::warning::warning:
コメントにて指摘をいただきました。
上の形だと、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()を使うと以下のようになります。

代替コード(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に関連あるプロパティであることを明示できるので、ControlEventBinderで使うと良いと思います。

元のコード
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の場合はイベントが流れてきた処理も一緒に書けるのでオススメです。

代替コード(ReactiveのExtension使用)
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()を使うことでフラグを消すことができます。

代替コード(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()を使ってフラグを消します。

代替コード(skip使用)
let viewDidAppear = PublishRelay<Void>() // viewDidAppearのタイミングで発火されるイベント
viewDidAppear
    .take(1) // 最初の1回以降は無視
    .subscribe(onNext: { result in
        // 何かしらの処理
    })
    .disposed(by: disposeBag)

エラーが出たときは型を注意深く確認する

最後は開発中の話ですが、Rx関連でコンパイラが出すエラーは当てにならないことが多いです。
なにかエラーが出たときは、ストリームを流れるイベントの型を注意深く追ってみましょう。

おわりに

自分の経験を元に注意すべき点やこうすればもっと良くなるという点をまとめてみました:pencil:
まだまだ自分も勉強中なので、もっと気をつける点などあると思います:warning:
是非そういったところや経験談などをコメントいただけると嬉しいです!:bow:

39
21
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
39
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?