RxSwiftを触ってみる


ずっと気になっていて使ったことがなかったので、今更ながらRxSwiftを触ってみました。

RxSwiftとは

C#用のライブラリReactive Extensionsが様々な言語に派生しているうちのSwift版となります。

非同期イベントを受け取る為の枠組みが用意されており、様々な処理を簡易に書けるのが強みの模様。
学習コストが高いらしいですが、他の言語でも同じように書けるようなのでその辺りは良いのかもしれません。

RxSwiftの導入

私はCarthageで導入しました。

Cartfileに以下を記載します。

Cartfile
github "ReactiveX/RxSwift"

下記のコマンドを実行します。

$ carthage update --platform iOS --no-use-binaries
*** Fetching RxSwift
*** Checking out RxSwift at "4.4.0"
*** xcodebuild output can be found in /var/folders/qg/qs42kxfj49z3bly2_f9v0cdh0000gn/T/carthage-xcodebuild.2pAGBX.log
*** Building scheme "RxAtomic" in Rx.xcworkspace
*** Building scheme "RxBlocking" in Rx.xcworkspace
*** Building scheme "RxCocoa" in Rx.xcworkspace
*** Building scheme "RxSwift" in Rx.xcworkspace
*** Building scheme "RxTest" in Rx.xcworkspace

ビルドされた.frameworkをxcodeprojへ追加します。

[TARGETS]-[General]-[Linked Frameworks and Library]の+ボタンから、[Add Other...]で追加した.frameworkを選択します。
今回はRxSwift.frameworkRxCocoa.frameworkを追加しました。

[TARGETS]-[Build Phases]の+ボタンから、[New Run Script Phase]で項目を追加します。
追加された項目のshellに以下を記載

/usr/local/bin/carthage copy-frameworks

InputFilesに以下を追加

$(SRCROOT)/Carthage/Build/iOS/RxSwift.framework
$(SRCROOT)/Carthage/Build/iOS/RxCocoa.framework

以上でRxSwiftを利用する準備は完了

使ってみる

まずはどんな感じなのか、簡単に使ってみます。
とりあえず、UITextFieldの入力をUILabelへ反映してみます。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {

    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var textField: UITextField!

    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        self.textField
            .rx
            .text
            .bind(to: self.label.rx.text)   // UILabelに反映
            .disposed(by: self.disposeBag)  // 不要になったら破棄
    }
}

これだけで、UITextFieldの入力内容がそのままUILabelに反映されます。
簡単です!!

既存の方法でやるなら UITextField.addTarget()UIControlEvents.EditingChangedを指定したりして、入力検知をコールバックで受け取ってUILabelに反映することになるかと思います。
UITextFieldが1、2つくらいなら問題ないですが、多いとコールバックが複数、もしくはコールバック内での分岐が増え分かりにくくなりますが、Rxだと繋がりが直感的に分かって良さげです。

UILabelへの反映前に処理をする

上記では、UITextFieldへの入力内容をそのままUILabelへ反映しましたが、反映前に処理を追加して反映内容を操作することもできるようです。

例えば、以下のようにすると文字数をカウントしその内容をUILabelに反映できます。

self.textField
    .rx
    .text
    .map({ "文字数:\(($0 ?? "").count)" }) // 文字数をカウント
    .bind(to: self.label.rx.text)
    .disposed(by: self.disposeBag)

入力内容のバリデーションなどをリアルタイムで実行したり、色々とできそうです。

.disposed(by:)について

bind(to:)を呼び出した後には、適切に解除しないとメモリリークに繋がってしまいます。
その為の解除処理が、.disposed(by: self.disposeBag)になります。
これを行うことで、self.disposeBagが破棄されるタイミングで同時に解除されます。

それ以外にも以下のようにすると、任意のタイミングで解除することもできます。

let disposable = self.textField
    .rx
    .text
    .bind(to: self.label.rx.text)

// 任意のタイミングで実施する
disposable.dispose()

これだと解除のタイミングを全て管理する必要があり漏れる可能性もありますので、基本的には、.disposed(by:)を使った方が良さげです。

モデルとのバインド

UI同士のバインドはでしたが、今度はモデルとUIとのバインドのサンプルです。

import UIKit
import RxSwift
import RxCocoa

struct Model {
    let item = BehaviorRelay<String?>(value: "Hello!")
}

class ViewController: UIViewController {

    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var textField: UITextField!

    private var model = Model()

    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        // UITextField -> Model.itemをバインド
        self.textField
            .rx
            .text
            .bind(to: self.model.item)
            .disposed(by: self.disposeBag)

        // Model.item -> UILabelをバインド
        self.model.item
            .bind(to: self.label.rx.text)
            .disposed(by: self.disposeBag)
    }
}

これで、UITextFieldに入力された値がModel.itemに反映され、その後にUILabelにも反映されます。

最後に

今回触った範囲は、RxSwiftの初歩中の初歩です。
まだまだ色々とできそうなので、触った所感を記事にできたらなと思います。