iOS
翻訳
Swift
RxSwift

[翻訳] こんにちはRxSwift!(RxSwift-Chinese-Documentation)

中国語で書かれたRxSwiftのドキュメントが非常に参考になりました。
他の方のRxSwiftの勉強に少しでも役に立てばと思い、日本語に翻訳してみました!
今回は2章の「こんにちはRxSwift!」となります。

原文: https://beeth0ven.github.io/RxSwift-Chinese-Documentation/content/first_app.html

最初のRxSwiftアプリケーション

これはユーザーログイン手順のシミュレーションです。

  • ユーザー名を入力するとき、5文字未満の場合は赤いエラーメッセージが表示され、パスワードを入力できません。
  • 5文字未満のパスワードを入力した場合も、赤いエラーメッセージが表示されます。
  • ユーザー名とパスワードの要件を満たす場合は緑色のボタンをタップすることができるが、両方の要件を満たさない場合はタップすることが出来ません。
  • 緑色のボタンをタップすると、AlertViewが表示されます。

このサンプルをダウンロードしてシミュレータで実行すると、プログラム全体の相互作用を理解するのに役立ちます。

SimpleValidationFull.gif

この画面は主に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!
    ...
}

ここでは、4つのやりとりを完了させる必要があります。

1. ユーザー名が5文字未満の場合はメッセージを表示し、パスワードを入力出来ないようにする

スクリーンショット 2017-12-19 1.48.34.png

override func viewDidLoad() {
    super.viewDidLoad()

    ...

    // ユーザ名が有効か
    let usernameValid = usernameOutlet.rx.text.orEmpty
        .map { $0.characters.count >= minimalUsernameLength }
        .share(replay: 1)

    ...

    // ユーザ名が有効 -> パスワード入力可能
    usernameValid
        .bind(to: passwordOutlet.rx.isEnabled)  
        .disposed(by: disposeBag)

    // ユーザ名が有効 -> メッセージを非表示
    usernameValid
        .bind(to: usernameValidOutlet.rx.isHidden)
        .disposed(by: disposeBag)

    ...
}

ユーザーがユーザー名入力ボックスの内容を変更すると新しいユーザー名が生成されます。mapメソッドでユーザ名が有効であるかのBool値に変換され、最終的にbind(to: ...)メソッドでパスワード入力が可能か、メッセージが非表示か決まります。

2. パスワードが5文字未満の場合、メッセージを表示する

スクリーンショット 2017-12-19 1.47.43.png

override func viewDidLoad() {
    super.viewDidLoad()

    ...

    // パスワードが有効か
    let passwordValid = passwordOutlet.rx.text.orEmpty
        .map { $0.characters.count >= minimalPasswordLength }
        .share(replay: 1)

    ...

    // パスワードが有効 -> メッセージ非表示
    passwordValid
        .bind(to: passwordValidOutlet.rx.isHidden)
        .disposed(by: disposeBag)

    ...
}

これは、ユーザー名でメッセージを制御するのに使用されるのと同じロジックです。

3. ユーザー名とパスワードの要件を満たしたら、緑ボタンを有効にする

スクリーンショット 2017-12-19 22.35.08.png

override func viewDidLoad() {
    super.viewDidLoad()

    ...

    // ユーザ名が有効か
    let usernameValid = ...

    // パスワードが有効か
    let passwordValid = ...

    ...

    // 全ての入力が有効か
    let everythingValid = Observable.combineLatest(
          usernameValid,
          passwordValid
        ) { $0 && $1 } // ユーザ名とパスワードが有効
        .share(replay: 1)

    ...

    // 全ての入力が有効 -> 緑色のボタンが有効
    everythingValid
        .bind(to: doSomethingOutlet.rx.isEnabled)
        .disposed(by: disposeBag)

    ...
}

Observable.combineLatest(...) { ... } は、ユーザー名とパスワードが有効かどうかを観測します。その結果を使い、緑色のボタンがタップ可能かどうかの制御を行います。

4. 緑色のボタンをタップしてAlertViewを表示する

スクリーンショット 2017-12-19 22.42.38.png

override func viewDidLoad() {
    super.viewDidLoad()

    ...

    // 緑色のボタンをタップ -> AlertView表示
    doSomethingOutlet.rx.tap
        .subscribe(onNext: { [weak self] in self?.showAlert() })
        .disposed(by: disposeBag)
}

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

    alertView.show()
}

緑色のボタンをタップした後にAlertViewを表示しています。

以上の4つのやりとりが完了し、次はこのプログラムがどのような構造であるか全体像を見ていきます。

以下が全体のコードとなります。

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.orEmpty
        .map { $0.characters.count >= minimalUsernameLength }
        .share(replay: 1)

    let passwordValid = passwordOutlet.rx.text.orEmpty
        .map { $0.characters.count >= minimalPasswordLength }
        .share(replay: 1)

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

    usernameValid
        .bind(to: passwordOutlet.rx.isEnabled)
        .disposed(by: disposeBag)

    usernameValid
        .bind(to: usernameValidOutlet.rx.isHidden)
        .disposed(by: disposeBag)

    passwordValid
        .bind(to: passwordValidOutlet.rx.isHidden)
        .disposed(by: disposeBag)

    everythingValid
        .bind(to: doSomethingOutlet.rx.isEnabled)
        .disposed(by: disposeBag)

    doSomethingOutlet.rx.tap
        .subscribe(onNext: { [weak self] in self?.showAlert() })
        .disposed(by: disposeBag)
}

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

    alertView.show()
}

このような複雑なやり取りは、数行のコードで行うことができます。これにより、開発効率を大幅に向上させることができます。

その他の質問

  • share(replay: 1) は何のためですか?
    usernameValid を使用して、ユーザー名のメッセージが非表示になっているかどうか、およびパスワードの入力が可能かどうかを制御します。shareReplay は、新しいソースを作成するのではなく、このソースを共有できるようにすることです。これにより、不要な生成処理が削減されます。
  • disposed(by: disposeBag) は何のためですか?
    使い慣れたオブジェクトと同様に、各バインドにもライフサイクルがあります。そして、バインドはクリアすることができます。 disposed(by: disposeBag) は、管理する disposeBag へのバインディングのライフサイクルです。 Disposebag が解放されると、まだクリアされていないバインドがクリアされます。 これは、ARC を使用してバインディングのライフサイクルを管理することと同じです。
    この内容については、Disposableのセクションで詳しく説明します。

(翻訳が間違ってたら教えてくださいm(_ _)m)