LoginSignup
24
16

More than 5 years have passed since last update.

それ、RxSwiftで書いてみましょう(入門編)

Last updated at Posted at 2018-12-16

はじめに

この記事は株式会社アイエンターAdvent Calendar 2018、17日目の記事です。

普段私はモバイルエンジニアとして主にiOS開発やってますが、もはやRxSwift使わないと開発できないカラダになってしまいました。
できることなら社内のプロジェクトにどんどんRxを取り入れていきたいと考えています。
しかし、どのように便利なのか分かりづらい、学習コストに見合うだけの効果があるのかといった声もチラホラ聞くので社内への布教の意味も込めて記事に残したいと思います。

RxSwiftとは?

iOS開発でリアクティブプログラミングを実現するSwiftライブラリです。
RxAndroidやRxJava、RxKotlinなどのAndroid版もあります。
リアクティブプログラミングとは簡単に説明すると、データの変化やイベントなどを一つの川の流れのようにストリームとして扱うことで、宣言的に処理を書くことができます。
タップイベントであろうと非同期な通信処理であろうと宣言的に書くことができます。
それの何が良いか言うと、データの変化やイベント発生時に何が起こるのかが宣言的にまとまっているのでコードの見通しが良くなります。

リアクティブプログラミングって?

Rxやリアクティブプログラミングの概念について詳しく知りたい方は、私が説明するまでもなく丁寧に解説してくださっている記事が世に多数あるので概念等はそちらをご参照ください。
おすすめはこちらの記事です。
オブザーバーパターンから始めるRxSwift入門

勉強しないといけないんでしょう?

はい、学習コストは高いです。臆せず勉強しましょう。
ただ、昨今のスマホネイティブアプリ開発において導入事例は多数あるので有識者は多いと思います。
困ったら近くのRxマンに色々聞いてみてください。

サンプル

では早速ですがここからはRxSwiftを使わない従来どおりの実装方法と、RxSwiftを使った実装の事例を紹介していきます。
Observable, Subject, 各種Traitsや各コードの詳細な説明は省略します。

入力した文字列をラベルにリアルタイムに反映する

01.gif

よくあるやつですね。これをまずRx無しで実装してみると以下のような感じになると思います。

ViewController.swift
class ViewController: UIViewController {
    // 結果出力用ラベル
    @IBOutlet weak var outputLabel: UILabel!
    // 入力用テキストフィールド
    @IBOutlet weak var inputTextField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()

        inputTextField.addTarget(self,
                                 action: #selector(textFieldEditingChanged(sender:)),
                                 for: .editingChanged)
    }

    @objc private func textFieldEditingChanged(sender: UITextField) {
        outputLabel.text = sender.text
    }
}

テキストフィールドに入力された値を監視して、ラベルに反映するだけのコードです。ごく一般的な実装方法だと思います。

では次にRxを使って書いてみます。以下のようになります。

ViewControllerRx.swift
class ViewControllerRx: UIViewController {
    @IBOutlet weak var outputLabel: UILabel!
    @IBOutlet weak var inputTextField: UITextField!
    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        inputTextField.rx.text
            .bind(to: outputLabel.rx.text)
            .disposed(by: disposeBag)
    }

}

宣言的に記述することができ、スッキリしました。
しかし要件が簡単だったため、まだRxの恩恵を受けているとは言えないですね。
ここで以下のような仕様追加があったとしましょう。

指定文字数以上は入力できないようにしてほしい

02.gif

登録フォームのバリデーションなどでよくあるやつですね。
先程のコードに加筆修正する形で、Rxを使用しない従来どおりのパターンで実装してみると以下のようになると思います。

ViewController.swift
class ViewController: UIViewController {
    @IBOutlet weak var outputLabel: UILabel!
    @IBOutlet weak var inputTextField: UITextField!

    private let maxLength: Int = 10

    override func viewDidLoad() {
        super.viewDidLoad()

        inputTextField.delegate = self
        inputTextField.addTarget(self,
                                 action: #selector(textFieldEditingChanged(sender:)),
                                 for: .editingChanged)
    }

    @objc private func textFieldEditingChanged(sender: UITextField) {
        if let text = sender.text, text.count > maxLength {
            sender.text = text.prefix(maxLength).description
        }
        outputLabel.text = sender.text
    }
}

extension ViewController: UITextFieldDelegate {
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
}

テキストフィールドへの入力完了後にキーボードを閉じるようにし、最大入力可能文字数以上入力された場合は、文字列の最大桁数でテキストの内容を上書くようにしてみました。
実際に取り入れる場合にはいくつか考慮すべき点があるとは思いますがひとまず要件を満たしています。

では次にRxを使って実装してみたいと思います。

ViewControllerRx.swift
class ViewControllerRx: UIViewController {
    @IBOutlet weak var outputLabel: UILabel!
    @IBOutlet weak var inputTextField: UITextField!

    private let maxLength: Int = 10
    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        inputTextField.rx.text.subscribe(onNext: { text in
            if let text = text, text.count >= self.maxLength {
                self.inputTextField.text = text.prefix(self.maxLength).description
            }
            self.outputLabel.text = self.inputTextField.text
        }).disposed(by: disposeBag)
    }

}

やっていることは同じですが、Rxを使った場合では処理が各関数に散り散りになることなく、スッキリ書けています。
ただこの程度であればRxを使わなくても良いんじゃないの?という気もしてきます。

入力可能文字数の表示とボタン押下制御

ということで一気に以下の仕様が追加されたとしましょう。

  • 入力可能文字数をリアルタイムに表示したい
  • 入力可能文字数に達した場合は、文字色を変更する
  • 一定文字数以上入力しなければボタンが押下できない
  • ボタンをタップしたら次の画面に遷移する

03.gif

例のごとく、Rxを使わない一般的な実装を行ってみます。

ViewController.swift
class ViewController: UIViewController {
    @IBOutlet weak var outputLabel: UILabel!
    @IBOutlet weak var inputTextField: UITextField!
    @IBOutlet weak var remainTextCountLabel: UILabel!
    @IBOutlet weak var button: UIButton!

    private let maxLength: Int = 10
    private let minimumTextLength: Int = 6
    private let normalTextColor: UIColor = .black
    private let limitedTextColor: UIColor = .red

    override func viewDidLoad() {
        super.viewDidLoad()

        button.setBackgroundImage(UIImage(color: .lightGray), for: .disabled)

        setRemainCount(text: inputTextField.text)
        changeTextColorBy(limit: maxLength, textField: inputTextField)
        changeButtonEnableBy(length: minimumTextLength, textField: inputTextField)

        inputTextField.delegate = self
        inputTextField.addTarget(self,
                                 action: #selector(textFieldEditingChanged(sender:)),
                                 for: .editingChanged)
    }

    /// ボタンがタップされたとき
    @IBAction func buttonTapped(_ sender: UIButton) {
        if let viewController = UIStoryboard(name: "SecondViewController", bundle: nil)
            .instantiateInitialViewController() as? SecondViewController {
            navigationController?.pushViewController(viewController, animated: true)
        }
    }

    /// テキストフィールドに入力された文字列に変化があったときに実行される
    @objc private func textFieldEditingChanged(sender: UITextField) {
        if let text = sender.text, text.count > maxLength {
            sender.text = text.prefix(maxLength).description
        }
        outputLabel.text = sender.text

        setRemainCount(text: sender.text)
        changeTextColorBy(limit: maxLength, textField: inputTextField)
        changeButtonEnableBy(length: minimumTextLength, textField: inputTextField)
    }

    /// あと何文字入力できるかを表示する
    private func setRemainCount(text: String?) {
        let text = text ?? ""
        let remainCount = maxLength - text.count
        remainTextCountLabel.text = "あと\(remainCount)文字入力できます"
    }

    /// 入力された文字数が制限数を超えた場合に文字色を変更する
    private func changeTextColorBy(limit: Int, textField: UITextField) {
        guard let text = textField.text else { return }

        textField.textColor = (text.count >= limit) ? limitedTextColor : normalTextColor
    }

    /// 入力されている文字数に応じてボタンの押下制御を行う
    private func changeButtonEnableBy(length: Int, textField: UITextField) {
        if let text = textField.text, text.count >= length {
            button.isEnabled = true
        } else {
            button.isEnabled = false
        }
    }
}

extension ViewController: UITextFieldDelegate {
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
}

コードが長くなってしまいました。
各主要機能を関数に集約させてみたのですが、いかがでしょうか。
画面初期表示時のことを考慮して各機能を関数にしてロード完了時に実行するようにしています。
パッと見で処理全体の流れを掴むのが難しい気がします。
今回はタップイベントも含まれるので@IBActionを定義している箇所と実際に処理されている関数が離れているのも見づらさに拍車をかけているのではないでしょうか。
では次にRxを使った実装を見てみましょう。

ViewControllerRx.swift
class ViewControllerRx: UIViewController {
    @IBOutlet weak var outputLabel: UILabel!
    @IBOutlet weak var inputTextField: UITextField!
    @IBOutlet weak var remainTextCountLabel: UILabel!
    @IBOutlet weak var button: UIButton!

    private let maxLength: Int = 10
    private let minimumTextLength: Int = 6
    private let normalTextColor: UIColor = .black
    private let limitedTextColor: UIColor = .red
    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        button.setBackgroundImage(UIImage(color: .lightGray), for: .disabled)

        // 入力内容変更時
        inputTextField.rx.text.subscribe(onNext: { text in
            // 文字列長によってテキストカラーを変える
            if let text = text, text.count >= self.maxLength {
                self.inputTextField.text = text.prefix(self.maxLength).description
                self.inputTextField.textColor = self.limitedTextColor
            } else {
                self.inputTextField.textColor = self.normalTextColor
            }
            // ラベルに入力された文字列を反映する
            self.outputLabel.text = self.inputTextField.text

            // 入力されている文字数を取得
            let inputTextLength = self.inputTextField.text?.count ?? 0

            // あと何文字入力できるかを表示する
            let remainCount = self.maxLength - inputTextLength
            self.remainTextCountLabel.text = "あと\(remainCount)文字入力できます"

            // ボタンの押下制御
            self.button.isEnabled = (inputTextLength >= self.minimumTextLength)
        }).disposed(by: disposeBag)

        // ボタンタップ時
        button.rx.tap.subscribe(onNext: {
            if let viewController = UIStoryboard(name: "SecondViewController", bundle: nil)
                .instantiateInitialViewController() as? SecondViewController {
                self.navigationController?.pushViewController(viewController, animated: true)
            }
        }).disposed(by: disposeBag)
    }

}

無駄に関数化していない点もありますが、viewDidLoad内で全て完結させることができています。
入力されたテキストが変化したときボタンがタップされたときとで宣言的に記述できているので処理の流れも負いやすいのではないかなと個人的に思っています。

Rxを使わないパターンでは、意図的に関数を作って処理をあっちこっちに委譲し合うかたちで記載していますが、ここにNotificationやデリゲートパターンなどがからんでくるとどうしても実際の処理内容が飛び飛びになって分かりづらくなってしまうケースはよくあるのではないでしょうか。

まとめ

今回は簡単な例でRxを使うとどのような実装になるかといった紹介でしたが、正直なところこれだけの機能であればRxを使う必要はないと思います。
Rxが本領発揮する場面は今回紹介できなかった非同期処理やMVVM、Clean Architectureなどのアーキテクチャが採用されている場合などです。
RxSwiftのほんの一部分のみ紹介しましたが、まずRxを使うことで何ができて、何が嬉しいのかといった本質を理解して、本当に導入する必要があるのかなどを考慮する必要があるのかなと個人的には考えています。

個人アプリで開発者も自分だけ!といったケースなら好みで使えば良いと思いますが、それなりに学習コストも必要になるのでチームに導入する際はプロジェクトの性質や体制、規模などをよく検討してからにすることをオススメします!

次回はAPI通信などの非同期処理やみんな大好きUITableViewでの活用方法などを紹介できればと思います。

おまけ

今回使ったコードはGitHubにあげてます。
https://github.com/ie-naraki/RxSwiftExamples01

24
16
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
24
16