LoginSignup
14
14

More than 1 year has passed since last update.

【Swift】0から始めるRxSwift。とりあえず動くものを作らせてくれ編。

Last updated at Posted at 2023-02-18

はじめに

RxSwift。難しいですよねぇ。ということで、今回はObervable?Observer?そんな小難しい言葉の説明をする前にとりあえず簡単に動くものを作らせてくれよ。という人のために記事を書きます。

こんな人に読んでもらいたい

  • RxSwiftそろそろやってみようと思ってるけどなぁという人
  • とりあえずRxSwiftを使ってなにがしかのコードを書きたい人

説明しないこと

  • ObservableやObserverなどの言葉の意味
  • RxSwiftを用いたアーキテクチャに関すること

この記事を読んで得られること

「お、RxSwift使って動くものが一応できた!」っていう感覚が味わえます。
ぜひ実際に手を動かしてコードを書いてみてください。

今回作るサンプル

今回作るサンプルアプリは以下のようなものです。
とてもシンプルです。

サンプルの仕様

  • 以下の2つの条件をクリアしている場合、ボタンが押せるようになる
    • 2つのTextFieldそれぞれに3文字以上入力されている
    • 2つのTextFieldに入力されている文字が同じである
  • ボタンをタップするとラベルの文字が変更される

実際のサンプル

Simulator Screen Recording - iPhone 14 Pro - 2023-02-18 at 15.34.31.gif

まずはRxを使わない実装

これは特に説明することもないかなと思うので、コードだけ貼っておきます。

final class WithoutRxViewController: UIViewController {

    @IBOutlet private weak var textField1: UITextField!
    @IBOutlet private weak var textField2: UITextField!
    @IBOutlet private weak var label: UILabel!
    @IBOutlet private weak var button: UIButton! {
        didSet {
            button.isHidden = true
        }
    }

    @IBAction private func tappedButton(_ sender: UIButton) {
        label.text = "押されたぁぁぁ"
    }

    @IBAction func changedTextField1(_ sender: UITextField) {
        button.isHidden = !isValid()
    }

    @IBAction func changedTextField2(_ sender: UITextField) {
        button.isHidden = !isValid()
    }

    private func isValid() -> Bool {
        let text1 = textField1.text ?? ""
        let text2 = textField2.text ?? ""
        return text1.count > 2 &&  text2.count > 2 && text1 == text2
    }
}

RxSwiftを用いた実装

import UIKit
import RxSwift
import RxCocoa

final class WithRxViewController: UIViewController {

    @IBOutlet private weak var button: UIButton!
    @IBOutlet private weak var label: UILabel!
    @IBOutlet private weak var textField1: UITextField!
    @IBOutlet private weak var textField2: UITextField!

    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        button.rx.tap
            .subscribe( onNext: { [weak self] in
                guard let strongSelf = self else { return }
                strongSelf.label.text = "押されたぁぁぁ"
            })
            .disposed(by: disposeBag)

        Observable
            .combineLatest(textField1.rx.text.orEmpty,
                           textField2.rx.text.orEmpty)
            .subscribe(onNext: { [weak self] text1, text2 in
                guard let strongSelf = self else { return }
                strongSelf.button.isHidden = !strongSelf.isValid(text1: text1, text2: text2)
            })
            .disposed(by: disposeBag)
    }

    private func isValid(text1: String, text2: String) -> Bool {
        text1.count > 2 &&  text2.count > 2 && text1 == text2
    }
}

特徴をざっくり解説

IBActionがないだと⁈

はい。気が付きましたか?RxSwiftを使うとタップイベント、TextFieldの文字が変更されたときのイベントをIBActionを使わずに取得することができます。これは楽ですね。

Buttonのタップイベントを取得する

以下のように書けます。.subscribeの中にはボタンがタップされた時の処理を書きます。
今回はlabelの文字を変更するようにしています。

     button.rx.tap
            .subscribe( onNext: { [weak self] in
                // タップされた時の処理を書く。
                guard let strongSelf = self else { return }
                strongSelf.label.text = "押されたぁぁぁ"
            })
            .disposed(by: disposeBag)

TextFieldの文字が変化した時に文字列を取得する

例えばtextField1の値が変化したときの文字を取得したければ以下のように書けます。
文字が変更されるたびに.subscribeの中は呼ばれます。

     textField1.rx.text
            .subscribe( onNext: {
                // 文字を取得して、ここで処理できます。
                print($0)
            })
            .disposed(by: disposeBag)

ただし、上記の書き方だと取得できる文字はOptionalです。
もしアンラップした文字列を取得したい場合は、以下のように.orEmptyを使用することで実現できます。

     textField1.rx.text.orEmpty
            .subscribe( onNext: {
                print($0)
            })
            .disposed(by: disposeBag)

Observableのとこ何⁈

はいはい。ここですね。多分一番意味わからないところは。

      Observable
            .combineLatest(textField1.rx.text.orEmpty,
                           textField2.rx.text.orEmpty)
            .subscribe(onNext: { [weak self] text1, text2 in
                guard let strongSelf = self else { return }
                strongSelf.button.isHidden = !strongSelf.isValid(text1: text1, text2: text2)
            })
            .disposed(by: disposeBag)

順番に説明しましょう。

先ほど以下のコードではtextField1の文字が変化した時に呼ばれると説明しました。

     textField1.rx.text.orEmpty
            .subscribe( onNext: {
                // textField1の文字列が変化するたびに呼ばれる。
                print($0)
            })
            .disposed(by: disposeBag)

今回のサンプルではtextField1が変更された時にtextField1の文字列を取得したいですが、このとき一緒にtextField2の文字列も取得したいです。逆も然りです。
なぜ片方のTextFieldの値が変更したとき、もう片方のTextFieldの文字列も取得したいのでしょう?
これは、ボタンをタップできるかどうかの判定は両方のTextFieldの文字を使って行うから。
だから、どちらか片方の文字列に変更があったら、もう片方の文字列も取得したいんですね。

そんな時に使えるのが.combineLatest
これを使うことで、textField1textField2のどちらかが変更されたときに、両方のTextFieldの文字を一緒に取得することができます。

いかがでしょう?
では、もう一度見てみましょう。
ここのコードでtextField1textField2をまとめて、どちらかに変更があったら教えてね。というようにできます。

     Observable
            .combineLatest(textField1.rx.text.orEmpty,
                           textField2.rx.text.orEmpty)

そして、以下のように.subscribeをつけてあげることで、どちらかが変更された時に両方の文字列を取得できます。ちなみにtextFieldは3つ4つと増やすこともできます。

あとは、取得した文字列を使用して、ボタンを表示するかしないかを判定、ボタンのisHiddenに値を代入してあげればOKですね。なんとなくイメージできたでしょうか?

 Observable
            .combineLatest(textField1.rx.text.orEmpty,
                           textField2.rx.text.orEmpty)
            .subscribe(onNext: { [weak self] text1, text2 in
                // text1とtext2にそれぞれの文字列が入っている
                guard let strongSelf = self else { return }
                strongSelf.button.isHidden = !strongSelf.isValid(text1: text1, text2: text2)
            })
            .disposed(by: disposeBag)

viewDidLoad⁉︎

まず、このコードを見てviewDidLoadにしかボタンのタップイベント文字列の変化ボタンの状態の変化に関する処理がないということにお気づきでしょうか?

画面が生成される時に1度しかこのコードは呼ばれません。
でも、文字列が変更されるたびに処理は呼ばれます。ボタンタップ時の処理が呼ばれるのはviewDidLoadが呼ばれた時ではありません。不思議ですね。

これは、イメージ的にはExcelが近いと思っています。
C4のセルに画像のような関数を設定しておくと、A4B4のセルに数値を入力すると、自動的にC4のセルの値も書き変わります。

つまりこれは、C4には〇〇が起きたら、△△してね。という処理の流れだけが登録されている状態になっているということです。
その結果、A4の値が変更されたらC4の値は変わるし、B4の値が変化したらC4も変化します。

スクリーンショット 2023-02-18 18.40.11.png

これと同じように、例えば以下のコードは「Buttonがタップされた時には△△してね」という処理の流れだけを登録しています。
だから、viewDidLoadで一度だけ呼べばボタンがタップされるたびに処理は行われるということですね。

     button.rx.tap
            .subscribe( onNext: { [weak self] in
                // タップされた時の処理を書く。
                guard let strongSelf = self else { return }
                strongSelf.label.text = "押されたぁぁぁ"
            })
            .disposed(by: disposeBag)

textFieldについても同様に、「値が変更したときにはバリデーションにかけて、ボタンの状態を変更してね」という処理の流れのみを登録しておけば、あとは勝手に動いてくれるわけです。

diposeBagってなに⁈

今回は説明しません!!
とりあえずは呪文だという認識でいいと思います。

おまけ

一応Rxを使ったコードはこんな感じにも書き換えられるよという紹介です。
さっきのと全然ちゃうやんけーという感じのコードですね。
これも全部説明すると長くなるので今回はやめておきます。

    override func viewDidLoad() {
        super.viewDidLoad()

        Observable
            .combineLatest(textField1.rx.text.orEmpty,
                           textField2.rx.text.orEmpty)
            .map { self.isValid(text1: $0, text2: $1) }
            .map { !$0 }
            .bind(to: button.rx.isHidden)
            .disposed(by: disposeBag)

        button.rx.tap
            .map { "押されたぁぁぁ" }
            .bind(to: label.rx.text)
            .disposed(by: disposeBag)
    }

今回のサンプルコード

もし必要であれば実際のサンプルコードも見ていってください。
https://github.com/Rin-t/SampleRx

おわりに

RxSwiftむずかしいぃぃぃ。
おわり。

14
14
0

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
14
14