Reputation: 119
I'm using MVVM pattern and try to show warning labels only when user press the sign in button. Now they don't appear, because I don't know how to show them on user action only. Then warning for the corresponding label should hide while user starts to edit it. Here is my ViewController which handle reference to viewModel:
import UIKit
import RxSwift
import RxCocoa
class RxLoginViewController: UIViewController {
@IBOutlet weak var signInButton: UIButton!
@IBOutlet weak var phoneNumberTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var phoneWarningLabel: UILabel!
@IBOutlet weak var passwordWarningLabel: UILabel!
fileprivate var viewModel: RxLoginViewModel?
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
viewModel = RxLoginViewModel(phoneNumber: phoneNumberTextField.rx.text.orEmpty.asDriver(), passwordText: passwordTextField.rx.text.orEmpty.asDriver())
addBindsToViewModel(viewModel: viewModel!)
setupButtons()
}
fileprivate func addBindsToViewModel(viewModel: RxLoginViewModel) {
phoneNumberTextField.rx.text
.orEmpty
.asObservable()
.debug("phoneNumberTextField")
.bindTo(viewModel.phoneNumberText)
.addDisposableTo(disposeBag)
passwordTextField.rx.text
.orEmpty
.asObservable()
.debug("passwordTextField")
.bindTo(viewModel.passwordText)
.addDisposableTo(disposeBag)
viewModel.showPhoneWarning
.asDriver()
.debug("showPhoneWarning")
.drive(onNext: { [weak self] showWarning in
UIView.animate(withDuration: 0.2) {
self?.phoneWarningLabel.isHidden = !showWarning
}}
)
.addDisposableTo(disposeBag)
viewModel.showPasswordWarning
.asDriver()
.debug("showPasswordWarning")
.drive(onNext: { [weak self] showWarning in
UIView.animate(withDuration: 0.2) {
self?.passwordWarningLabel.isHidden = !showWarning
}
})
.addDisposableTo(disposeBag)
viewModel.credentialsValid
.debug("credentialsValid")
.drive(onNext: { [weak self] valid in
self?.signInButton.isEnabled = valid
self?.signInButton.alpha = valid ? 1 : 0.5
})
.addDisposableTo(disposeBag)
}
private func setupButtons() {
signInButton.rx.tap
.bindTo(viewModel!.signInAction)
.addDisposableTo(disposeBag)
}
}
And ViewModel:
class RxLoginViewModel {
let dataManager = DataManager.sharedInstance()
private let disposeBag = DisposeBag()
//MARK: - Model proprties
var phoneNumberText = Variable<String>("")
var passwordText = Variable<String>("")
var signInAction: Variable<Void> = Variable<Void>()
var showPhoneWarning = Variable(false)
var showPasswordWarning = Variable(false)
var credentialsValid: Driver<Bool>
var canLogIn = Variable(false)
init(phoneNumber: Driver<String>, passwordText: Driver<String>) {
let phoneValid = phoneNumber
.distinctUntilChanged()
.throttle(0.3)
.map { ($0 =~ RegEx.phone) }
let passwordValid = passwordText
.distinctUntilChanged()
.throttle(0.3)
.map { ($0.utf8.count > 5) }
credentialsValid = Driver.combineLatest(phoneValid, passwordValid) { $0 && $1 }
phoneNumber.debug("phone number driver")
.drive(onNext: {_ in self.showPhoneWarning.value = false })
.addDisposableTo(disposeBag)
phoneNumber.debug("password driver")
.drive(onNext: {_ in self.showPasswordWarning.value = false })
.addDisposableTo(disposeBag)
credentialsValid.asObservable()
.bindTo(canLogIn)
.addDisposableTo(disposeBag)
// actions handler
signInAction
.asObservable()
.debug("signInAction")
.do(onNext: { _ in
// show warning for any invalid textfield
// filter both valid fields
} )
// sent url request
.subscribe(onNext: { status in
self.dataManager.login(withPhone: self.phoneNumberText.value, email: "", password: self.passwordText.value, success: { ( result ) in
if let response = result as? [String : Any], response["aboutMe"] == nil {
self.dataManager.currentUser().userId = response["id"] as? NSNumber
// self.dataManager.update(.fillProfile)
}
}, failure: { (error) in
// print(error.localizedDescription)
})
})
.addDisposableTo(disposeBag)
}
What should I do to show warning for any invalid textfield and how to filter both valid fields in my ViewModel?
Upvotes: 0
Views: 2749
Reputation: 33967
I don't do my view models the same way you do, but hopefully this will help you out...
The key here is to outline everything that can affect the showPhoneWarning
observable.
For example, you want the observable to emit true if signInAction
comes in while phoneNumberText
isn't valid.
Let's express that as a variable:
let invalidPhoneNumberOnTap = source.phoneNumberText
.map { $0.isValidPhoneNumber == false }
.sample(source.signInAction)
You also want the observable to emit false if phoneNumberText
fires a new element.
let hideWarning = source.phoneNumberText.map { _ in false }
Now all you have to do is merge those two together. You also want to make sure it starts with false
so the warning doesn't show prematurely.
showPhoneWarning = Observable.of(invalidPhoneNumberOnTap, hideWarning)
.merge()
.startWith(false)
Handle the showEmailWarning in much the same way.
Upvotes: 3