#はじめに
RxSwiftのexampleをコードで書く
RxSwift公式のSimpleValidationViewController.swiftの話です
レイアウトはsnapKitを使っています
#環境構築
プロジェクトファイルを作る
名前は何でもいいですが今回の例ではRxSwift_for_code
としてます
podfileの編集
$ vi Podfile
Podfileの中身
target 'RxSwift_for_code' do
のRxSwift_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文字以上になった時 | ボタンを押すと |
---|---|---|
#コードの中身
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型の値を取り出せる
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?
のSelf
はControlPropertyType
を指す
定義に飛ぶと下記のようになっている
/// 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?
のE
はpublic 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をちゃんと書いてお行儀良くストリームを購読しよう
理解不足が多々あると思うので
間違っていたらご指摘お願いします