RxSwift
swift4

RxSwiftのexampleをコードで書く(その2)

はじめに

RxSwiftのexampleをコードで書く
RxSwift公式のSimpleValidationViewController.swiftの話です
レイアウトはsnapKitを使っています

環境構築

プロジェクトファイルを作る

名前は何でもいいですが今回の例ではRxSwift_for_codeとしてます

podfileの編集

$ vi Podfile

Podfileの中身
target 'RxSwift_for_code' doRxSwift_for_codeをプロジェクトファイルで使った名前にしてください

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'

use_frameworks!

target 'RxSwift_for_code' do
    pod 'RxSwift',    '~> 4.0'
    pod 'RxCocoa',    '~> 4.0'
    pod 'SnapKit', '~> 4.0.0'
end

Podfileに登録したライブラリをおとしてくる (bundlerでcocoapodを管理しているためpodのコマンドを使うときはbundle execを前につける必要がある)

$ pod install

どんなサンプルなの?

TextFieldに入れる文字数によってlabelのtextを表示するかしないかを制御できる

ユーザーネームとパスワードどちらとも5文字以上にならないとボタンがおせないような制御ができる

文字が5文字以上になる前 5文字以上になった時 ボタンを押すと
screenshot.png screenshot.png screenshot.png

コードの中身

import UIKit
import RxSwift
import RxCocoa
import SnapKit

fileprivate let minimalUsernameLength = 5
fileprivate let minimalPasswordLength = 5

class ViewController: UIViewController {
    var disposeBag = DisposeBag()

    var userName: UILabel = {
        let label = UILabel()
        label.text = "userName"
        return label
    }()

    var userNameField: UITextField = {
        let textField = UITextField()
        textField.borderStyle = .roundedRect
        return textField
    }()

    var userNameValidLable: UILabel = {
        let label = UILabel()
        label.text = "userName"
        return label
    }()

    var password: UILabel = {
        let label = UILabel()
        label.text = "password"
        return label
    }()

    var passwordField: UITextField = {
        let textField = UITextField()
        textField.borderStyle = .roundedRect
        return textField
    }()

    var passwordValidLable: UILabel = {
        let label = UILabel()
        label.text = "password"
        return label
    }()

    var button: UIButton = {
        let button = UIButton()
        button.backgroundColor = UIColor.blue
        button.setTitle("テスト", for: .normal)
        return button
    }()


    override func viewDidLoad() {
        super.viewDidLoad()
        self.initializeUILayout()
        userNameValidLable.text = "ユーザー名は\(minimalUsernameLength) 文字以上"
        passwordValidLable.text = "パスワードは\(minimalPasswordLength) 文字以上"

        let userNameValid = userNameField.rx.text.orEmpty
            .map{$0.count >= minimalUsernameLength}
            .share(replay: 1)

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

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

        userNameValid.bind(to: passwordField.rx.isEnabled).disposed(by:disposeBag)

        userNameValid.bind(to: userNameValidLable.rx.isHidden).disposed(by:disposeBag)

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

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

        button.rx.tap.subscribe(onNext: {[weak self] _ in self?.showAlert()})

    }

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

        alertView.show()
    }

    func initializeUILayout() {
        self.view.addSubview(self.userName)
        self.userName.snp.makeConstraints { (make) -> Void in
            make.width.equalTo(300)
            make.height.equalTo(50)
            make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(10)
            make.centerX.equalTo(self.view)
        }
        self.view.addSubview(self.userNameField)
        self.userNameField.snp.makeConstraints { (make) -> Void in
            make.width.equalTo(300)
            make.height.equalTo(50)
            make.top.equalTo(self.userName.snp.bottom)
            make.centerX.equalTo(self.view)
        }
        self.view.addSubview(self.userNameValidLable)
        self.userNameValidLable.snp.makeConstraints { (make) -> Void in
            make.width.equalTo(300)
            make.height.equalTo(50)
            make.top.equalTo(self.userNameField.snp.bottom)
            make.centerX.equalTo(self.view)
        }

        self.view.addSubview(self.password)
        self.password.snp.makeConstraints { (make) -> Void in
            make.width.equalTo(300)
            make.height.equalTo(50)
            make.top.equalTo(self.userNameValidLable.snp.bottom)
            make.centerX.equalTo(self.view)
        }
        self.view.addSubview(self.passwordField)
        self.passwordField.snp.makeConstraints { (make) -> Void in
            make.width.equalTo(300)
            make.height.equalTo(50)
            make.top.equalTo(self.password.snp.bottom)
            make.centerX.equalTo(self.view)
        }
        self.view.addSubview(self.passwordValidLable)
        self.passwordValidLable.snp.makeConstraints { (make) -> Void in
            make.width.equalTo(300)
            make.height.equalTo(50)
            make.top.equalTo(self.passwordField.snp.bottom)
            make.centerX.equalTo(self.view)
        }

        self.view.addSubview(self.button)
        self.button.snp.makeConstraints { (make) -> Void in
            make.width.equalTo(300)
            make.height.equalTo(50)
            make.top.equalTo(self.passwordValidLable.snp.bottom)
            make.centerX.equalTo(self.view)
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}

調べたことメモ

土管を作っているイメージに近いと思った

userNameValidのところ

let userNameValid = userNameField.rx.text.orEmpty
            .map{$0.count >= minimalUsernameLength}
            .share(replay: 1)

.text

getということは値を取り出せること
textの型はRxCocoa.ControlProperty<String?>

extension Reactive where Base : UITextField {
/// Reactive wrapper for `text` property.
    public var text: RxCocoa.ControlProperty<String?> { get }

.orEmpty

.text次に.orEmptyがつながる

定義はこのようになっている
変数orEmptyはgetなのでString型が中にはいったControlProperty型の値を取り出せる
```Swift
extension ControlPropertyType where Self.E == String? {

/// Transforms control property of type `String?` into control property of type `String`.
public var orEmpty: RxCocoa.ControlProperty<String> { get }

}
``
また
ControlPropertyType型のextensionでwhere Self.E == String?SelfControlPropertyType`を指す

定義に飛ぶと下記のようになっている

/// Protocol that enables extension of `ControlProperty`.
public protocol ControlPropertyType : ObservableType, ObserverType {

    /// - returns: `ControlProperty` interface
    func asControlProperty() -> ControlProperty<E>
}

//////省略///////

public struct ControlProperty<PropertyType> : ControlPropertyType {
    public typealias E = PropertyType

    let _values: Observable<PropertyType>
    let _valueSink: AnyObserver<PropertyType>
//////省略///////

where Self.E == String?Epublic typealias E = PropertyTypeのEを指す
今回の例では具体的にorEmptyをつなげるため定義を再び見てみると下記のようになっていて
orEmpty: RxCocoa.ControlProperty<String>
ControlProperty<E>と一致している

.map{$0.count >= minimalUsernameLength}

map関数では流れてきたデータをそれぞれ比較している

.count

$0にはStringの型のデータがはいってくる、流れてきたデータ(String型)に対して、その数を数えてIntで返す関数である

extension String {
 public var count: Int { get }
}

この数とminimalUsernameLengthを比較し,trueとfalseを流していく

.share(replay: 1)

ここが参考になりました
[RxSwift] shareReplayをちゃんと書いてお行儀良くストリームを購読しよう

複数のObserverが購読してるストリームで、最初の例の様に2本のストリームを作るのではなく、
1本ストリームから同じ値を購読したいんだ、という場合には不必要な処理や意図しない処理が走らないように、shareReplay(share)をつけておきましょう!

ということでした。

この土管ではStringの入力に対して、文字数を数え、その文字数によってtrueかfalseに変換されるという流れができていることがわかりました

userNameValidを購読するところ

先ほどできた土管を使ってほかの土管を制御している

.bind(to: passwordField.rx.isEnabled).disposed(by:disposeBag)

userNameValidはtrueかfalseが流れてくるはず
このtrueかfalseの値を使ってisEnabled(触れる),isHidden(隠れる)のプロパティを制御している

新しいサブスクリプションを作成し、オブザーバに要素を送信します。
- パラメータ:イベントを受け取るオブザーバー。
(ここではpasswordFieldのisEnabledのプロパティのこと)
- returns:オブザーバの登録を解除するために使用できる使い捨てオブジェクト。
(返り値は登録を解除できるオブジェクトを返すから後ろに.disposedをつなげることができる)

extension ObservableType {
    public func bind<O>(to observer: O) -> Disposable where O : ObserverType, Self.E == O.E

.isEnabled

定義を確認するとBinder<Bool>の型のプロパティであることがわかる

extension Reactive where Base: UIControl {

    /// Bindable sink for `enabled` property.
    public var isEnabled: Binder<Bool> {
        return Binder(self.base) { control, value in
            control.isEnabled = value
        }
    }

buttonのtapの制御のところ

button.rx.tap.subscribe(onNext: {[weak self] _ in self?.showAlert()})
ボタンがタップされたときにどんな関数を実行するのかクロージャー内にまとめている

.tap

UIButtonクラスを拡張するReactiveextension
tapの型はControlEvent<Void>で流れてくるのはVoidの型

extension Reactive where Base: UIButton {

    /// Reactive wrapper for `TouchUpInside` control event.
    public var tap: ControlEvent<Void> {
        return controlEvent(.touchUpInside)
    }
}

subscribe

subscribeの定義は下記のようになっている

public func subscribe(onNext: ((Self.E) -> Swift.Void)? = default, onError: ((Error) -> Swift.Void)? = default, onCompleted: (() -> Swift.Void)? = default, onDisposed: (() -> Swift.Void)? = default) -> Disposable

第一引数はonNext: ((Self.E) -> Swift.Void)? = default
引数はクロージャー、もし引数がなければdefaultが設定されるようになっているため、この引数はなくてもいい
第二引数はonError: ((Error) -> Swift.Void)? = default
今回は使わなかったが失敗した場合はここにクロージャーを入れるといい感じに処理をしてくれるのだろう

{[weak self] _ in self?.showAlert()}

クロージャー内でリファレンスカウンタを増やさないために弱参照を設定している
引数はonNext: ((Self.E) -> Swift.Void)? = defaultより(Self.E)具体的にはtapControlEvent<Void>でVoidなのではないかな・・?
このクロージャー内部ではこの引数は使わないので '_'で引数は存在するが名前をつけないようにしている
弱参照のため、クラス内の関数を呼び出す際はselfではなくself?をつけて呼んでいる

参考文献

[RxSwift] shareReplayをちゃんと書いてお行儀良くストリームを購読しよう

理解不足が多々あると思うので
間違っていたらご指摘お願いします