TodoAPPでRxSwift入門[part3]


概要

最近RxSwiftを勉強し始めて現在理解していることを備忘録として残せたらいいなと思い記事にします。
そもそもRxSwiftのRxとは

Rx(Reactive X)とは、「オブザーバパターン」「イテレータパターン」「関数型プログラミング」の概念を実装している拡張ライブラリです。
Rxを導入するメリットは、「値の変化を検知できる」「非同期の処理を簡潔に書ける」ということに尽きると思います。 値の変化というのは変数値の変化やUIの変化も含まれます。 例えばボタンをタッチする、という動作もボタンのステータスが変わったと捉えることができRxを使って記述することができます。

とのことです。
詳しくは以下のサイトを参照してください。
入門!RxSwift
RxSwiftについてようやく理解できてきたのでまとめることにした(1)

今回はPart3になります。
前回の記事はこちら
TodoAPPでRxSwift入門[part2]

AddTodoViewController

前回の予告通りViewControllerの中身をみていきましょう。

AddTodoViewController.swift
import UIKit
import RxSwift
import RxCocoa

class AddTodoViewController: UIViewController {

    @IBOutlet weak var titleField: UITextField!
    @IBOutlet weak var detailView: UITextView!
    @IBOutlet weak var joinButton: UIButton!
    @IBOutlet weak var showTodoListButton: UIButton!

    let disposeBag = DisposeBag()

    private var viewModel: AddTodoViewPresentable!

    override func viewDidLoad() {
        super.viewDidLoad()
        viewModel = AddTodoViewModel(input: (
            titleText: titleField.rx.text.orEmpty.asDriver(),
            detailText: detailView.rx.text.orEmpty.asDriver()
        ), storeManager: StoreManager.shared)

        setupViews()
        setupBinding()
    }

}

private extension AddTodoViewController {

    private func setupViews() {
        titleField.borderStyle = .none
    }

    private func setupBinding() {

        viewModel.output.isValid
            .drive(joinButton.rx.isEnabled)
            .disposed(by: disposeBag)

        viewModel.output.isValid.drive(onNext: { [weak self] isValid in
            self?.joinButton.backgroundColor = isValid ? .init(red: 200/255, green: 200/255, blue: 255/255, alpha: 1) : .lightGray
        }).disposed(by: disposeBag)

        titleField.rx.controlEvent(.editingDidBegin).asDriver().drive(onNext: { [weak self] in
            self?.firstResponderAnimate()
        }).disposed(by: disposeBag)

        titleField.rx.controlEvent(.editingDidEnd).asDriver().drive(onNext: { [weak self] in
            self?.resignFirstResponderAnimate()
        }).disposed(by: disposeBag)

        detailView.rx.didBeginEditing.asDriver().drive(onNext: { [weak self] in
            self?.firstResponderAnimate()
        }).disposed(by: disposeBag)

        detailView.rx.didEndEditing.asDriver().drive(onNext: { [weak self] in
            self?.resignFirstResponderAnimate()
        }).disposed(by: disposeBag)

        joinButton.rx.tap.subscribe(onNext: { [weak self] in
            guard let title = self?.titleField.text, let detail = self?.detailView.text else { return }
            self?.viewModel.insertTodoToFireStore(title: title, detail: detail)
            self?.titleField.text = ""
            self?.detailView.text = ""
        }).disposed(by: disposeBag)

        showTodoListButton.rx.tap.subscribe(onNext: { [weak self] in
            let viewController = TodoListViewController.instantiate()
            let navigationController = UINavigationController(rootViewController: viewController)
            navigationController.modalPresentationStyle = .fullScreen
            self?.present(navigationController, animated: true)
        }).disposed(by: disposeBag)
    }

    private func firstResponderAnimate() {
        let width = view.frame.size.width
        UIView.animate(withDuration: 0.3) { [weak self] in
            self?.view.transform = CGAffineTransform(translationX: 0, y: -width / 4)
        }
    }

    private func resignFirstResponderAnimate() {
        UIView.animate(withDuration: 0.3) { [weak self] in
            self?.view.transform = .identity
        }
    }
}

まずViewModelを初期化しているところからいきます。
titleText, detailTextにそれぞれの入力するUIを紐付けます。
orEmptyはアンラップ的な役割になります。

Input = (
   text: Driver<String>, ()
)

があったとしてStringを指定しているのに実際流れてくるのはString?なのでorEmptyがないとエラーになると思います。

次にsetupBindingの中をみていきます。
drivebind(to: )のようなものです。
viewModelisValidの値とjoinButton.rx.isEnabledを紐付けています。
これによって入力の文字数が足りていなかったりした時にボタンが押せないようにしたりできます。

次にcontrolEventですがこれは入力モードになった時にonNextで画面を少し上にあげるというような処理になります。

textViewも同様ですが、controlEventではなく直接どうなったかの状態を指定する形?になっています。

ボタンのところはタップした時の処理ですね。
タップしたらFirestoreにデータを保存しています。

まとめ

今回の記事を書いて思ったことはFatViewControllerを避けるためのMVVMなのに割とFatになってしまったなという感じです。
Inputでボタンがタップされたかどうかをobserveしてデータ保存の処理とかを完全にViewModelに任せてしまいたいですがまだまだ勉強が足りないですね。
また画面遷移はcoordinatorパターンですると画面遷移の処理もViewModelで検知して画面遷移するというのもできそうな気がするのですが、MVVMのそれぞれの責務を考えたときそれはViewの仕事なのかな?とも思います。
難しいところですが精進します。

次回は保存したTodoを表示します。
多分次回で終わりにします。

改善できるところや指摘がございましたらよろしくお願いします。

次はこちら。
TodoAPPでRxSwift入門[part4]