RxSwift と RxCocoa を用いた MVVM パターンによるリアクティブ UI 実装

UIKit におけるユーザーインターフェースの状態管理を、リアクティブプログラミングで簡潔に実現する方法について解説します。ここでは RxSwift、RxCocoa、および MVVM アーキテクチャを組み合わせて、動的なデータバインディングを構築します。

要件

  • 画面には3つのテキストフィールド(UITextField)と1つのラベル(UILabel)を配置
  • ラベルには、3つのフィールドに入力された数値の合計がリアルタイムに表示される
  • 数値以外の文字列が入力された場合、その値は0として扱う
  • 各テキストフィールドの初期値はそれぞれ "1", "2", "3"

プロジェクト作成

Xcode を起動し、「File」→「New」→「Project…」から新しいプロジェクトを作成します。テンプレート選択画面で「iOS」→「Single View App」を選択。次に Product Name を RxFormDemo など適切な名前に設定し、任意の場所に保存してプロジェクトを作成します。一度 Xcode を閉じます。

CocoaPods による依存関係の設定

プロジェクトディレクトリ内に Podfile を作成し、以下の内容を記述します。

use_frameworks!

target 'RxFormDemo' do
  pod 'RxSwift', '~> 5.1'
  pod 'RxCocoa', '~> 5.1'
  pod 'RxBinding', '~> 0.4'
  pod 'NSObject+Rx', '~> 5.1.0'
end
    

ターミナルで以下を実行してライブラリをインストールします。

cd RxFormDemo
pod install
    

インストール完了後、生成された RxFormDemo.xcworkspace を開いて開発を再開します。

双方向バインディング演算子の拡張

カスタム演算子 <~> を追加することで、BehaviorRelay<String> と UI コンポーネント間の双方向同期を簡潔に記述できるようにします。別ファイルまたは ViewController.swift 内に以下を追加します。

import RxBinding

public func <~> (relay: BehaviorRelay<String>, input: TextInput<UIControl>) -> Disposable {
    return relay.twoWayBind(to: input)
}
    

Storyboard 上での UI 構成

Main.storyboard を開き、View Controller のビュー内に以下のコンポーネントを配置:

  • UITextField × 3
  • UILabel × 1(テキストを空または初期値として設定)

IBOutlet の接続

ViewController クラスに以下のプロパティを追加します。

class ViewController: UIViewController {
    @IBOutlet private var fieldA: UITextField!
    @IBOutlet private var fieldB: UITextField!
    @IBOutlet private var fieldC: UITextField!
    @IBOutlet private var sumLabel: UILabel!

    private var viewModel: SumViewModel!
    private let disposeBag = DisposeBag()
}
    

Assistant Editor を使用して、それぞれの UI 要素を対応する IBOutlet に接続します。

ViewModel の実装

MVVM パターンに基づき、UI 非依存のロジックを担う ViewModel を定義します。このクラスは入力値の変化を監視し、合計を計算して出力します。

import RxSwift

class SumViewModel {
    let input1 = BehaviorRelay<String>(value: "1")
    let input2 = BehaviorRelay<String>(value: "2")
    let input3 = BehaviorRelay<String>(value: "3")
    let total = BehaviorRelay<String>(value: "6")

    private let disposeBag = DisposeBag()

    init() {
        Observable.combineLatest(input1, input2, input3) { a, b, c in
            let valA = Int(a) ?? 0
            let valB = Int(b) ?? 0
            let valC = Int(c) ?? 0
            return valA + valB + valC
        }
        .map(String.init)
        .bind(to: total)
        .disposed(by: disposeBag)
    }
}
    

ViewController におけるバインディングの設定

ビューDidLoad で ViewModel を初期化し、UI コンポーネントと双方向・単方向バインディングを行います。

override func viewDidLoad() {
    super.viewDidLoad()

    viewModel = SumViewModel()

    // 双方向バインディング:ViewModel ↔ TextField
    viewModel.input1 <~> fieldA.rx.textInput ~ disposeBag
    viewModel.input2 <~> fieldB.rx.textInput ~ disposeBag
    viewModel.input3 <~> fieldC.rx.textInput ~ disposeBag

    // 単方向バインディング:ViewModel → Label
    viewModel.total ~> sumLabel.rx.text ~ disposeBag
}
    

上記コード中の ~ 演算子は、RxBinding により提供される構文糖で、生成された Disposable を自動的に DisposeBag に追加します。

タグ: RxSwift RxCocoa MVVM ReactiveX UIKit

6月11日 19:30 投稿