【RxSwift】BehaviorRelay/PublishSubjectを使ってみた


# はじめに
RxSwiftの入門書を読みながら個人的に理解した内容を書き出して見ようと思います。

## 環境
Xcode 12.4
Swift 5.3.2

# BehaviorRelay/PublishSubjectを使ってみた

RxSwiftの入門書を読みながらPublish/Behavior + Relay/Subjectの組み合わせを上手に使いたいと思いましたが、とても混乱しました。

||Relay　.next(.accept)のみ|Subject　全イベント|
|:|-|--|
|Publish　 初期値なし|PublushRelay .valueなし|PublishSubject (value:)なし |
|Behavior　初期値あり|BehaviorRelay　.valueあり|BehaviorSubject (value:)あり|
結果的に、値を保持させる(もしくはViewに表示させる)場合はBehaviorRelay、
ボタンタップなどのイベントはPublishSubjectと割り切って実装してみました。

以下実装の流れ。

- BehaviorRelayの初期値をラベルに表示
- ボタンタップイベントをPublishSubjectへ流す
- PublishSubjectをsubscribeしてBehaviorRelay.acceptで更新値を流す
- BehaviorRelayの更新値をラベルに表示

```swift
class MainViewController: UIViewController {
    let viewModel = MainViewModel()
    var disposeBag = DisposeBag()
    @IBOutlet private weak var groupLabel: UILabel!
    @IBOutlet private weak var groupButton: UIButton!

    override func viewDidLoad {
        super.viewDidLoad
        disposeBag.insert(
            viewModel.groupName.bind(to: gropuLabel.rx.text),
            groupButton.rx.tap.bind(to: viewModel.groupButtonPressed)
        }
    }
}

class MainViewModel {
    var disposeBag = DisposeBag()
    var groupName: BehaviorRelay<String> = BehaviorRelay(value: "グループA")
    let groupButtonPressed = PublishSubject<Void>()

    override init() {
        super.init()
        mutate()
    }
    func mutate() {
        groupButtonPressed.subscribe(onNext: { [weak self] _ in
            guard let self = self else { return }
            groupName.accept("グループB")
        }).disposed(by: disposeBag)
    }
}
```


# 画面遷移の処理を書いてみた

実装の流れは下記の通り。

- ボタンタップでイベントをViewModelへ流す
- ViewModelでボタンタップイベントを観察して画面遷移の値を流す
- ViewControllerで画面遷移の値を観察しておく事で、値を受け取り画面遷移させる

```swift
class MainViewController: UIViewController {
    var viewModel = MainViewModel()
    var disposeBag = DisposeBag()
    @IBOutlet private weak var cameraButton: UIButton!
    @IBOutlet private weak var movieButton: UIButton!
    override func viewDidLoad {
        super.viewDidLoad
        disposeBag.insert(
            cameraButton.rx.tap.bind(to: viewModel.cameraButtonSelect),
            movieButton.rx.tap.bind(to: viewModel.movieButtonSelect)
        }
        viewModel.transition.subscribe(onNext: { [weak self] transition in
            guard let self = self else { return }
            switch transition {
            case .camera:
                let storyboard = UIStoryboard(name: "Camera", bundle: nil)
                let vc = storyboard.instantiateViewController(identifier: "Camera")
                self.navigationController?.pushViewController(vc, animated: true)
            case .movie:
                let storyboard = UIStoryboard(name: "Movie", bundle: nil)
                let vc = storyboard.instantiateViewController(identifier: "Movie")
                self.navigationController?.pushViewController(vc, animated: true)
            case .none:
                break
            }
        }).disposed(by: disposeBag)
    }
}

enum MenuTransition {
    case camera
    case movie
}

class MainViewModel {
    var disposeBag = DisposeBag()
    let behaviorTransition: BehaviorRelay<MenuTransition?> = BehaviorRelay(value: nil)
    var transition: Observable<MenuTransition?> {
        return behaviorTransition.asObservable()
    }

    let cameraButtonSelect = PublishSubject<Void>()
    let movieButtonSelect = PublishSubject<Void>()

    override init() {
        super.init()
        mutate()
    }
    func mutate() {
        cameraButtonSelect.subscribe({ [weak self] _ in
            self?.behaviorTransition.accept(.camera)
        }).disposed(by: disposeBag)
        movieButtonSelect.subscribe({ [weak self] _ in
            self?.behaviorTransition.accept(.movie)
        }).disposed(by: disposeBag)
    }
}
```
`BehaviorRelay` は `Observable`ではないので、　`asObservable`が必要でした。
そうではなく、`let transition = PublishSubject<MenuTransition>()`とかでやっても良かったのかも。


# ログイン画面の処理を書いてみた

実装の流れは下記の通り。

- ボタンタップでイベントをViewModelへ流す
- ViewModelでボタンタップイベントを観察してログイン処理を開始する


```swift
class MainViewController: UIViewController {
    var viewModel = MainViewModel()
    var disposeBag = DisposeBag()
    @IBOutlet private weak var idTextField: UITextField!
    @IBOutlet private weak var passwordTextField: UITextField!
    @IBOutlet private weak var loginButton: UIButton!
    override func viewDidLoad {
        super.viewDidLoad
        disposeBag.insert(
            uuidTextField.rx.text.orEmpty.bind(to: viewModel.id),
            passwordTextField.rx.text.orEmpty.bind(to: viewModel.password),
            loginButton.rx.tap.bind(to: viewModel.loginAction)
        }
    }
}

class MainViewModel {
    var disposeBag = DisposeBag()

    let loginAction = PublishSubject<Void>()
    let id: BehaviorRelay<String> = BehaviorRelay(value: "")
    let password: BehaviorRelay<String> = BehaviorRelay(value: "")

    override init() {
        super.init()
        mutate()
    }
    func mutate() {
        let loginParam = Observable.combineLatest(id.asObservable(), password.asObservable()) { ($0, $1) }

        loginAction
            .withLatestFrom(loginParam)
            .flatMapLatest({ [weak self] (id, password) -> Observable<Result<LoginResponse, Error>> in
                guard let self = self else { return Observable.empty() }
                return self.loginRequest(id: id, password: password) 
            }).subscribe(onNext: { [weak self] result in
                guard let self = self else { return }
                switch result {
                case .success:
                    // ログイン成功処理・画面遷移
                case let .failure(error):
                    if let responseError = error as? ResponseError {
                    // ログイン失敗処理・画面遷移         
                    }
                }
            }).disposed(by: disposeBag)
    }

    func loginRequest(id: String, password: String) -> Observable<Result<LoginResponse, Error>> {
        // 下記でログイン処理
        return LoginManager.login(id: id, password: password)
    }
}
```
- `conbineLatest` で複数オブジェクトの最新状態を取得できる
- `BehaviorRelay` は `Observable`ではないので、　`asObservable`が必要
- `withLatestFrom` で、`①.withLatestFrom（②）`のような形にして、二つの要素（①と②）が揃うイベント実行するようになる。尚、イベントが実行されると要素は消える
- `flatMapLatest`イベントをObservableに変換して一つのObservableイベントを通知する。尚、新たなObservableが発行されると前の処理の完了を待たずに新しい方の処理を始める

# まとめ
実装できた部分については、しっかり記憶に定着させたいという目的で記事にしました。間違いありましたら、ご指摘いただけると助かります。


# 参考文献
- [オブザーバーパターンから始めるRxSwift入門](https://qiita.com/k5n/items/17f845a75cce6b737d1e)
- [guard let self = self ... を使おうとしているそこの貴方！ちょっとお待ちください！](https://qiita.com/peka2/items/c4c05b7c78a8b8ea4910)
- [DisposeBagに複数まとめて登録する時は.disposed(by:)を使うよりもinsert()を使う方が良い](https://qiita.com/417_72ki/items/5943700dbe5d3663512a)
- [よく使いそうなRxSwiftのオペレーターの勉強をしてみた](https://qiita.com/_xAGAx_/items/b3fe92c9f2a31e1a6ca5)