この投稿がQiitaデビューです*\(^o^)/*
RxSwiftのExamplesを読み解いてみました。
RxSwiftのレポジトリにある Why.md や GettingStarted.md は読んでみた。ストリームというのも読んで恐らく分かった。Qiitaの関係ある記事もちょくちょく読んでみた。次はiOSプロジェクトに活かそうと思っているが取っ掛かりが掴めないという人向け。(自分)
Adding Numbers
Observable.combineLatest(number1.rx_text, number2.rx_text, number3.rx_text) { textValue1, textValue2, textValue3 -> Int in
return (Int(textValue1) ?? 0) + (Int(textValue2) ?? 0) + (Int(textValue3) ?? 0)
}
.map { $0.description }
.bindTo(result.rx_text)
.addDisposableTo(disposeBag)
各UITextFieldの値が変更される度に、 combineLatest
の中で3つのUITextFieldに入力されているテキストをIntに変換し、足し算して、return。その結果を map
で String をとして返して、結果表示用のUILabelにバインドするだけで、UILabelが更新される!
Simple Validation
let usernameValid = usernameOutlet.rx_text
.map { $0.characters.count >= minimalUsernameLength }
.shareReplay(1) // (1
let passwordValid = passwordOutlet.rx_text
.map { $0.characters.count >= minimalPasswordLength }
.shareReplay(1)
let everythingValid = Observable.combineLatest(usernameValid, passwordValid) { $0 && $1 }
.shareReplay(1)
usernameValid
// パスワードフィールドの検証が真の時に, 有効になる
.bindTo(passwordOutlet.rx_enabled)
.addDisposableTo(disposeBag)
usernameValid
// ユーザー名フィールドの検証が真の時に, 非表示になる
.bindTo(usernameValidOutlet.rx_hidden)
.addDisposableTo(disposeBag)
passwordValid
// パスワードフィールドの検証が真の時に, 非表示になる
.bindTo(passwordValidOutlet.rx_hidden)
.addDisposableTo(disposeBag)
everythingValid
// usernameValid と passwordValid が共に真の場合に `doSomethingOutlet(UIButton)` が有効になる
.bindTo(doSomethingOutlet.rx_enabled)
.addDisposableTo(disposeBag)
doSomethingOutlet.rx_tap
// タップされた際に、`showAlert()`を呼ぶ
.subscribeNext { [weak self] in self?.showAlert() }
.addDisposableTo(disposeBag)
検証ロジックをユーザー名とパスワード用に定義し、それらを各コントロールにbind。検証結果により、UIButtonを有効にしたり、注意書きのUILabelを非表示にしたりしている。
shareReplay(1)
というのは、これがないとストリームが複数生成され、map
の中がUITextFieldの値を変更する度に、複数回実行されてしまう。詳しくは参考であげた記事が分かりやすいです。
Geolocation Subscription
let geolocationService = GeolocationService.instance
geolocationService.authorized
.drive(noGeolocationView.rx_driveAuthorization)
.addDisposableTo(disposeBag)
geolocationService.location
.drive(label.rx_driveCoordinates)
.addDisposableTo(disposeBag)
button.rx_tap
.bindNext { [weak self] in
self?.openAppPreferences()
}
.addDisposableTo(disposeBag)
button2.rx_tap
.bindNext { [weak self] in
self?.openAppPreferences()
}
.addDisposableTo(disposeBag)
GeolocationService
というのは、位置情報に関する便利クラス。 .authorized
では、位置情報の許可を取得しているか判定し、Boolで返してくれる。また、.location
では、位置情報が変更された時に呼び出されて、CLLocationCoordinate2Dで返してくれる。
bindTo
の代わりに drive
を使い、メインスレッドで結果を受け取るようにして、エラーも吐き出さないようにしている。Driveに関しては、本家の Unit が分かりやすい。
private extension UILabel {
var rx_driveCoordinates: AnyObserver<CLLocationCoordinate2D> {
return UIBindingObserver(UIElement: self) { label, location in
label.text = "Lat: \(location.latitude)\nLon: \(location.longitude)"
}.asObserver()
}
}
private extension UIView {
var rx_driveAuthorization: AnyObserver<Bool> {
return UIBindingObserver(UIElement: self) { view, authorized in
if authorized {
view.hidden = true
view.superview?.sendSubviewToBack(view)
}
else {
view.hidden = false
view.superview?.bringSubviewToFront(view)
}
}.asObserver()
}
}
この例では、カスタムObserver( rx_***
)の作成仕方を学べる。 rx_driveAuthorization
では、Boolを渡すようにして、その値により、UIViewのhiddenを切り替えたり、z軸を変更している。 rx_driveCoordinates
の方は、CLLocationCoordinate2D を結びつけその値により、UILabelのtextを変更している。
GitHub Signup - Vanilla Observables
let viewModel = GithubSignupViewModel1(
input: (
username: usernameOutlet.rx_text.asObservable(),
password: passwordOutlet.rx_text.asObservable(),
repeatedPassword: repeatedPasswordOutlet.rx_text.asObservable(),
loginTaps: signupOutlet.rx_tap.asObservable()
),
dependency: (
API: GitHubDefaultAPI.sharedAPI,
validationService: GitHubDefaultValidationService.sharedValidationService,
wireframe: DefaultWireframe.sharedInstance
)
)
// bind results to {
viewModel.signupEnabled
.subscribeNext { [weak self] valid in
self?.signupOutlet.enabled = valid
self?.signupOutlet.alpha = valid ? 1.0 : 0.5
}
.addDisposableTo(disposeBag)
viewModel.validatedUsername
.bindTo(usernameValidationOutlet.ex_validationResult)
.addDisposableTo(disposeBag)
viewModel.validatedPassword
.bindTo(passwordValidationOutlet.ex_validationResult)
.addDisposableTo(disposeBag)
viewModel.validatedPasswordRepeated
.bindTo(repeatedPasswordValidationOutlet.ex_validationResult)
.addDisposableTo(disposeBag)
viewModel.signingIn
.bindTo(signingUpOulet.rx_animating)
.addDisposableTo(disposeBag)
viewModel.signedIn
.subscribeNext { signedIn in
print("User signed in \(signedIn)")
}
.addDisposableTo(disposeBag)
//}
MVVMパターンの実装例。検証ロジックなどは、GithubSignupViewModel1
の中に入っている。GithubSignupViewModel1
を初期化する際に、ObserveするUITextFieldや検証するためのサービスを渡している。ViewControllerの中では、ValidationResult
によってテキストやテキスト色を変えるとうことをbindしているだけ。
GitHub Signup - Using Driver
class GithubSignupViewModel2 {
// outputs {
//
let validatedUsername: Driver<ValidationResult>
let validatedPassword: Driver<ValidationResult>
let validatedPasswordRepeated: Driver<ValidationResult>
// Is signup button enabled
let signupEnabled: Driver<Bool>
// Has user signed in
let signedIn: Driver<Bool>
// Is signing process in progress
let signingIn: Driver<Bool>
// }
init(
input: (
username: Driver<String>,
password: Driver<String>,
repeatedPassword: Driver<String>,
loginTaps: Driver<Void>
),
dependency: (
API: GitHubAPI,
validationService: GitHubValidationService,
wireframe: Wireframe
)
) {
let API = dependency.API
let validationService = dependency.validationService
let wireframe = dependency.wireframe
validatedUsername = input.username
.flatMapLatest { username in
return validationService.validateUsername(username)
.asDriver(onErrorJustReturn: .Failed(message: "Error contacting server"))
}
validatedPassword = input.password
.map { password in
return validationService.validatePassword(password)
}
validatedPasswordRepeated = Driver.combineLatest(input.password, input.repeatedPassword, resultSelector: validationService.validateRepeatedPassword)
let signingIn = ActivityIndicator()
self.signingIn = signingIn.asDriver()
let usernameAndPassword = Driver.combineLatest(input.username, input.password) { ($0, $1) }
signedIn = input.loginTaps.withLatestFrom(usernameAndPassword)
.flatMapLatest { (username, password) in
return API.signup(username, password: password)
.trackActivity(signingIn)
.asDriver(onErrorJustReturn: false)
}
.flatMapLatest { loggedIn -> Driver<Bool> in
let message = loggedIn ? "Mock: Signed in to GitHub." : "Mock: Sign in to GitHub failed"
return wireframe.promptFor(message, cancelAction: "OK", actions: [])
// propagate original value
.map { _ in
loggedIn
}
.asDriver(onErrorJustReturn: false)
}
signupEnabled = Driver.combineLatest(
validatedUsername,
validatedPassword,
validatedPasswordRepeated,
signingIn
) { username, password, repeatPassword, signingIn in
username.isValid &&
password.isValid &&
repeatPassword.isValid &&
!signingIn
}
.distinctUntilChanged()
}
}
GitHub Signup - Vanilla Observables
のDriverで実装したバージョン。ViewControllerは基本同じなので(driverの箇所以外)、ViewModelの中を見てみる。
GithubSignupViewModel2
では、大きく分けてinputとdependencyという引数を渡して初期化する。inputは、ObserveするUITextField等。dependencyでは、APIとの通信・検証機能・ポップアップを表示するサービス群を渡しています。こういったサービス群をキレイに分割して、保守性の高いコードになっています。
API wrappers
override func viewDidLoad() {
super.viewDidLoad()
datePicker.date = NSDate(timeIntervalSince1970: 0)
// MARK: UIBarButtonItem
bbitem.rx_tap
.subscribeNext { [weak self] x in
self?.debug("UIBarButtonItem Tapped")
}
.addDisposableTo(disposeBag)
// MARK: UISegmentedControl
// also test two way binding
let segmentedValue = Variable(0)
segmentedControl.rx_value <-> segmentedValue
segmentedValue.asObservable()
.subscribeNext { [weak self] x in
self?.debug("UISegmentedControl value \(x)")
}
.addDisposableTo(disposeBag)
// MARK: UISwitch
// also test two way binding
let switchValue = Variable(true)
switcher.rx_value <-> switchValue
switchValue.asObservable()
.subscribeNext { [weak self] x in
self?.debug("UISwitch value \(x)")
}
.addDisposableTo(disposeBag)
// MARK: UIActivityIndicatorView
switcher.rx_value
.bindTo(activityIndicator.rx_animating)
.addDisposableTo(disposeBag)
// MARK: UIButton
button.rx_tap
.subscribeNext { [weak self] x in
self?.debug("UIButton Tapped")
}
.addDisposableTo(disposeBag)
// MARK: UISlider
// also test two way binding
let sliderValue = Variable<Float>(1.0)
slider.rx_value <-> sliderValue
sliderValue.asObservable()
.subscribeNext { [weak self] x in
self?.debug("UISlider value \(x)")
}
.addDisposableTo(disposeBag)
// MARK: UIDatePicker
// also test two way binding
let dateValue = Variable(NSDate(timeIntervalSince1970: 0))
datePicker.rx_date <-> dateValue
dateValue.asObservable()
.subscribeNext { [weak self] x in
self?.debug("UIDatePicker date \(x)")
}
.addDisposableTo(disposeBag)
// MARK: UITextField
// also test two way binding
let textValue = Variable("")
textField <-> textValue
textValue.asObservable()
.subscribeNext { [weak self] x in
self?.debug("UITextField text \(x)")
}
.addDisposableTo(disposeBag)
// MARK: UIGestureRecognizer
mypan.rx_event
.subscribeNext { [weak self] x in
self?.debug("UIGestureRecognizer event \(x.state)")
}
.addDisposableTo(disposeBag)
// MARK: UITextView
// also test two way binding
let textViewValue = Variable("")
textView <-> textViewValue
textViewValue.asObservable()
.subscribeNext { [weak self] x in
self?.debug("UITextView text \(x)")
}
.addDisposableTo(disposeBag)
// MARK: CLLocationManager
#if !RX_NO_MODULE
manager.requestWhenInUseAuthorization()
#endif
manager.rx_didUpdateLocations
.subscribeNext { x in
print("rx_didUpdateLocations \(x)")
}
.addDisposableTo(disposeBag)
_ = manager.rx_didFailWithError
.subscribeNext { x in
print("rx_didFailWithError \(x)")
}
manager.rx_didChangeAuthorizationStatus
.subscribeNext { status in
print("Authorization status \(status)")
}
.addDisposableTo(disposeBag)
manager.startUpdatingLocation()
}
この例では、UIBarButtonItem
・UISegmentedControl
・UIDatePicker
・UISwitch
・UIActivityIndicatorView
・UIButton
・UISlider
・UIDatePicker
・UITextField
・UIGestureRecognizer
・UITextView
・CLLocationManager
といった様々なパーツに対してRxSwiftの使い方が分かります。
このサンプルでのポイントは、RxSwift独自の機能であるVariableだと思います。これは、BehaviorSubject
というものの薄いラッパーのようです。BehaviorSubject
は、subscribeした途端、イベントを送信し初期値を設定することができます。上記の例のVariable
では、各コントロールに対して、それぞれ初期値を与えています。UITextView
には、let textViewValue = Variable("")
といった感じです。
また、textView <-> textViewValue
として、双方向バインディング?というものをしているようです。これにより、textViewValue.value = "hogehoge"
とすることにより、UITextViewの表示も変更されます。
Calculator
override func viewDidLoad() {
let commands:[Observable<Action>] = [
allClearButton.rx_tap.map { _ in .Clear },
changeSignButton.rx_tap.map { _ in .ChangeSign },
percentButton.rx_tap.map { _ in .Percent },
divideButton.rx_tap.map { _ in .Operation(.Division) },
multiplyButton.rx_tap.map { _ in .Operation(.Multiplication) },
minusButton.rx_tap.map { _ in .Operation(.Subtraction) },
plusButton.rx_tap.map { _ in .Operation(.Addition) },
equalButton.rx_tap.map { _ in .Equal },
dotButton.rx_tap.map { _ in .AddDot },
zeroButton.rx_tap.map { _ in .AddNumber("0") },
oneButton.rx_tap.map { _ in .AddNumber("1") },
twoButton.rx_tap.map { _ in .AddNumber("2") },
threeButton.rx_tap.map { _ in .AddNumber("3") },
fourButton.rx_tap.map { _ in .AddNumber("4") },
fiveButton.rx_tap.map { _ in .AddNumber("5") },
sixButton.rx_tap.map { _ in .AddNumber("6") },
sevenButton.rx_tap.map { _ in .AddNumber("7") },
eightButton.rx_tap.map { _ in .AddNumber("8") },
nineButton.rx_tap.map { _ in .AddNumber("9") }
]
commands
.toObservable()
.merge()
.scan(CalculatorState.CLEAR_STATE) { a, x in
// ここで、CalculatorStateに変換している
return a.tranformState(x)
}
.debug("debugging")
.subscribeNext { [weak self] calState in
self?.resultLabel.text = self?.prettyFormat(calState.inScreen)
switch calState.action {
case .Operation(let operation):
switch operation {
case .Addition:
self?.lastSignLabel.text = "+"
case .Subtraction:
self?.lastSignLabel.text = "-"
case .Multiplication:
self?.lastSignLabel.text = "x"
case .Division:
self?.lastSignLabel.text = "/"
}
default:
self?.lastSignLabel.text = ""
}
}
.addDisposableTo(disposeBag)
}
この計算機の例では、Observable<Action>
の配列をmerge
して、1つのストリームとして扱います。そして、scan
という前の値をストリームで扱うものを使用します。Accumulatorとして、CalculatorState.CLEAR_STATE
から始まり、計算機のボタンを押す度にこれを更新していきます。また、debug
をはさみ都度結果をコンソールに出力。最後に、subscribeNextの中で、計算機上のUILabelを更新しています。このような計算機をViewControllerの中では、特に結果等を保持せずともほぼ上記のコードで実現してしまうのはすごいですね。
参考
RxSwift/Units.md at master · ReactiveX/RxSwift
https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Units.md#driver
RxSwift/Why.md at master · ReactiveX/RxSwift
https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Why.md
RxSwift/GettingStarted.md at master · ReactiveX/RxSwift
https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md
[RxSwift] shareReplayをちゃんと書いてお行儀良くストリームを購読しよう
http://qiita.com/kazu0620/items/bde4a65e82a10bd33f88
オブザーバーパターンから始めるRxSwift入門
http://qiita.com/k5n/items/17f845a75cce6b737d1e#variable