Takahiro Octopress Blog

-1から始める情弱プログラミング

RxSwiftを勉強してみよう!(1)

RxSwiftとは

Reactive Extensions(以下、Rx)のSwift版です。
と一言で言っても、「Rxとはなんぞや??」となってしまうかと思います。
(多分に漏れず、筆者もそうです…)
そこで、いろいろと調べてみると、

  • 「非同期/イベント/時間に関する処理をLINQの形式で簡潔かつ宣言的に記述すること」ができるのが特徴
  • 観測可能 (observable) なシーケンスと LINQ スタイルのクエリ演算子を使って、非同期なイベントベースのプログラムを合成するライブラリ

なんて話が出てきます。
確かに非同期処理はソースコードが複雑になりやすいので、何となくRxは良さそうな気がしてきました。
今回はまず触って慣れてみようということで見ていきましょう。

RxSwiftでUI

まずはRxSwiftの書き方を覚えるためにUI関連から見てきましょう。

サンプル画面

下記のように画面を作成します。
サンプル画面

RxSwiftのインストール

下記のようにPodfileを作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use_frameworks!

target 'RxSwiftSample' do
  pod 'RxSwift',    '~> 2.0'
  pod 'RxCocoa',    '~> 2.0'
end

target 'RxSwiftSampleTests' do
  pod 'RxBlocking', '~> 2.0'
  pod 'RxTests',    '~> 2.0'
end

target 'RxSwiftSampleUITests' do
  pod 'RxBlocking', '~> 2.0'
  pod 'RxTests',    '~> 2.0'
end

そして、pod installを実行します。

RxSwiftを用いたUIアクションを実装

下記のようにアクションを実装します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// ViewController.swift
import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {

  // UI部品
  @IBOutlet weak var sampleBtn: UIButton!
  @IBOutlet weak var sampleTextField: UITextField!
  @IBOutlet weak var sampleLabel: UILabel!

  // 自動unsubscribeのために必要
  let disposeBag = DisposeBag()

  override func viewDidLoad() {
      super.viewDidLoad()

      // ボタンタップアクション
      self.sampleBtn.rx_tap.subscribeNext { [unowned self] _ in
          self.showAlert()
      }.addDisposableTo(disposeBag)

      // テキストフィールドの入力アクション
      self.sampleTextField.rx_text
          .map { "Your Text is \($0)" }
          .bindTo(self.sampleTextField.rx_text)
          .addDisposableTo(disposeBag)
  }

  <省略>

  func showAlert() {
      let alert = UIAlertController(title: "Sample Alert", message: "Can you see a sample alert?", preferredStyle: .Alert)
      let ok = UIAlertAction(title: "OK", style: .Default) { (action) in
          // 特に何もしない
      }
      alert.addAction(ok)
      presentViewController(alert, animated: true, completion: nil)
  }
}

上記のUIアクションはあまりにも単純な例であるため、恩恵を受けられている気はしないですね…
いつものようにXcodeでアクションを繋げているのと同じ気がする…

RxSwiftの公式Exampleを覗いてみる

先ほどの例ではイマイチ良さがわからなかったので、公式のExampleを覗いてみます。
公式のExampleはGitHub: ReactiveX/RxSwiftからダウンロードできます。

NumbersViewController

最も簡単なExampleはこれになるかと思います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Foundation
import UIKit
import RxSwift
import RxCocoa

class NumbersViewController: ViewController {

  @IBOutlet weak var number1: UITextField!
  @IBOutlet weak var number2: UITextField!
  @IBOutlet weak var number3: UITextField!

  @IBOutlet weak var result: UILabel!

  override func viewDidLoad() {
      super.viewDidLoad()
      // 注目!!
      Observable.combineLatest(number1.rx_text, number2.rx_text, number3.rx_text) { textValue1, textValue2, textValue3 -> Int in
          return (Int(textValue1) ?? 0) + (Int(textValue2) ?? 0) + (Int(textValue3) ?? 0)
      }
      .map { $0.description }
      .bindTo(result.rx_text)
      .addDisposableTo(disposeBag)
  }
}

ここで注目したいのはObservable.combineLatestです。
Exampleではこれを利用して一気に3つのUITextFieldから値を取得して、合計値を計算しています。
Obsertvable.combineLatestは名前から察することができると思いますが、3つのUITextFieldのいずれかの値が変わったタイミングで合計値が再計算されます。

これだけでも少し便利な感じが伝わってきました。

SimpleValidationViewController

続いて着手しやすいExampleはこちらになりそうです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import Foundation
import UIKit
import RxSwift
import RxCocoa

let minimalUsernameLength = 5
let minimalPasswordLength = 5

class SimpleValidationViewController : ViewController {
  @IBOutlet weak var usernameOutlet: UITextField!
  @IBOutlet weak var usernameValidOutlet: UILabel!
  @IBOutlet weak var passwordOutlet: UITextField!
  @IBOutlet weak var passwordValidOutlet: UILabel!
  @IBOutlet weak var doSomethingOutlet: UIButton!

  override func viewDidLoad() {
      super.viewDidLoad()

      usernameValidOutlet.text = "Username has to be at least \(minimalUsernameLength) characters"
      passwordValidOutlet.text = "Password has to be at least \(minimalPasswordLength) characters"

      // 注目!!
      let usernameValid = usernameOutlet.rx_text
          .map { $0.characters.count >= minimalUsernameLength }
          .shareReplay(1)

      let passwordValid = passwordOutlet.rx_text
          .map { $0.characters.count >= minimalPasswordLength }
          .shareReplay(1)

      let everythingValid = Observable.combineLatest(usernameValid, passwordValid) { $0 && $1 }
          .shareReplay(1)

      usernameValid
          .bindTo(passwordOutlet.rx_enabled)
          .addDisposableTo(disposeBag)

      usernameValid
          .bindTo(usernameValidOutlet.rx_hidden)
          .addDisposableTo(disposeBag)

      passwordValid
          .bindTo(passwordValidOutlet.rx_hidden)
          .addDisposableTo(disposeBag)

      everythingValid
          .bindTo(doSomethingOutlet.rx_enabled)
          .addDisposableTo(disposeBag)

      doSomethingOutlet.rx_tap
          .subscribeNext { [weak self] in self?.showAlert() }
          .addDisposableTo(disposeBag)
  }

  func showAlert() {
      let alertView = UIAlertView(
          title: "RxExample",
          message: "This is wonderful",
          delegate: nil,
          cancelButtonTitle: "OK"
      )

      alertView.show()
  }
}

ここで注目したいのはshareReplay(1)という記述です。
このExampleでは、
usernameのバリデーションをクリアしていれば、passwordOutlet.rx_enabledusernameValidOutlet.rx_hiddenの2つを実行するように実装されています。

通常だと、usernameValidが2回実行されてしまうところをshareReplay(1)をつけることで、最適な回数だけ実行してくれます。
詳しくは、shareReplayをちゃんと書いてお行儀良くストリームを購読しようを読むと良いでしょう。

ここまで来るとRxSwiftの有効性を許容せざるを得ませんね。

まとめ

今回簡単に触っただけでも、Rxはかなり有効なものであると感じました。
非同期処理や互いに関連性を持つ複雑な処理を実装する際にはぜひRxを使ってみたいと思いました。

本当はもっとExampleを追っていきたいのですが、一旦ここまでとさせて頂きます。
(単なる時間切れなので、続きは必ず書きたいを思っています。)

ということで本日はここまで。

参考:

Comments