はじめに
RxSwift。難しいですよねぇ。ということで、今回はObervable?Observer?そんな小難しい言葉の説明をする前にとりあえず簡単に動くものを作らせてくれよ。という人のために記事を書きます。
こんな人に読んでもらいたい
- RxSwiftそろそろやってみようと思ってるけどなぁという人
- とりあえずRxSwiftを使ってなにがしかのコードを書きたい人
説明しないこと
- ObservableやObserverなどの言葉の意味
- RxSwiftを用いたアーキテクチャに関すること
この記事を読んで得られること
「お、RxSwift使って動くものが一応できた!」っていう感覚が味わえます。
ぜひ実際に手を動かしてコードを書いてみてください。
今回作るサンプル
今回作るサンプルアプリは以下のようなものです。
とてもシンプルです。
サンプルの仕様
- 以下の2つの条件をクリアしている場合、ボタンが押せるようになる
- 2つのTextFieldそれぞれに3文字以上入力されている
- 2つのTextFieldに入力されている文字が同じである
- ボタンをタップするとラベルの文字が変更される
実際のサンプル
まずは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
。
これを使うことで、textField1
、textField2
のどちらかが変更されたときに、両方のTextFieldの文字を一緒に取得することができます。
いかがでしょう?
では、もう一度見てみましょう。
ここのコードでtextField1
とtextField2
をまとめて、どちらかに変更があったら教えてね。というようにできます。
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
のセルに画像のような関数を設定しておくと、A4
、B4
のセルに数値を入力すると、自動的にC4
のセルの値も書き変わります。
つまりこれは、C4
には〇〇が起きたら、△△
してね。という処理の流れだけが登録されている状態になっているということです。
その結果、A4
の値が変更されたらC4
の値は変わるし、B4
の値が変化したらC4
も変化します。
これと同じように、例えば以下のコードは「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むずかしいぃぃぃ。
おわり。