I'm trying to understand some concepts of ReactiveCocoa 4 and does not find a way to correctly validate and send a request based on inputs from a login form.
My current solution sends requests on each valid update of my inputs, which is not good.
It seems that I need to use Actions
and CocoaActions
to fix my issue but I don't understand how to correctly implement them.
Here is my code example :
I want the login request to be sent when the Login Button is pressed and both the login and password fields are not empty, else I just display an Error.
Currently, the producer stays alive and continue to send requests when I modify the input fields, which is not good...
I would love an example on how to do it properly :)
LoginViewModel.swift
class LoginViewModel {
let login = MutableProperty<String>("")
let password = MutableProperty<String>("")
init() {
}
func logIn() -> SignalProducer<Int, IntranetError> {
return SignalProducer {
observer, disposable in
combineLatest(self.login.producer, self.password.producer)
.promoteErrors(Moya.Error)
.filter { (credentials : (String, String)) in
guard credentials.0.length > 0 else {
observer.sendFailed(IntranetError.MissingLoginError)
return false
}
guard credentials.1.length > 0 else {
observer.sendFailed(IntranetError.MissingPasswordError)
return false
}
return true
}
.flatMap(.Latest) { (credentials : (String, String)) -> SignalProducer<User, Moya.Error> in
let login = credentials.0
let password = credentials.1
return IntranetProvider.request(Intranet.LogIn(login, password)).filterSuccessfulStatusCodes()
.mapObject(User)
}
.start { (event) -> Void in
switch event {
case .Next(let user):
UserManager.sharedManager.user = user;
print(user)
observer.sendCompleted()
case .Failed(let error):
observer.sendFailed(.MoyaError(error))
default:
break
}
}
}
}
}
LoginViewController.swift
class LoginViewController: UIViewController, UITextFieldDelegate {
@IBOutlet weak var loginTextField: FramedTextField!
@IBOutlet weak var passwordTextField: FramedTextField!
@IBOutlet weak var connectButton: UIButton!
let viewModel : LoginViewModel = LoginViewModel()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.login <~ loginTextField.rac_text
viewModel.password <~ passwordTextField.rac_text
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@IBAction func connectButtonTouched(sender: AnyObject) {
loginAsked()
}
func loginAsked() -> Void {
SVProgressHUD.showWithMaskType(.Black)
viewModel.logIn().throttle(0.5, onScheduler: QueueScheduler.mainQueueScheduler).start { (event) in
switch event {
case .Completed:
SVProgressHUD.dismiss()
self.connectionSuccessfull()
case .Failed(let error):
switch error {
case .MissingPasswordError :
SVProgressHUD.showErrorWithStatus(NSLocalizedString("Missing password", comment: "User password is missing"))
case .MissingLoginError :
SVProgressHUD.showErrorWithStatus(NSLocalizedString("Missing login", comment: "User login is missing"))
case .MoyaError(let error) :
SVProgressHUD.showErrorWithStatus(error.toString())
default :
SVProgressHUD.showErrorWithStatus(NSLocalizedString("Internal error, please try again later", comment: ""))
}
default:
break
}
}
}
}
Thanks
Finally after a long night working on it I found how to implement it properly.
I was overthinking a bit the validation process in the end, and it's much simpler now.
Result :
LoginViewModel.swift
class LoginViewModel {
let login = MutableProperty<String>("")
let password = MutableProperty<String>("")
var loginAction : Action<(String, String), User, IntranetError>!
var cocoaActionLogin : CocoaAction!
init() {
loginAction = Action { (let login, let password) in
return SignalProducer {
observer, disposable in
guard login.length > 0 else {
observer.sendFailed(IntranetError.MissingLoginError)
return
}
guard password.length > 0 else {
observer.sendFailed(IntranetError.MissingPasswordError)
return
}
print("SENT")
IntranetProvider.request(Intranet.LogIn(login, password))
.filterSuccessfulStatusCodes()
.mapObject(User)
.start { (event) in
switch event {
case .Next(let user):
UserManager.sharedManager.user = user;
print(user)
observer.sendCompleted()
case .Failed(let error):
observer.sendFailed(.MoyaError(error))
default:
observer.sendCompleted()
}
}
}
}
cocoaActionLogin = CocoaAction(loginAction) { _ in
return (self.login.value, self.password.value)
}
}
}
LoginViewController.swift
class LoginViewController: UIViewController, UITextFieldDelegate {
@IBOutlet weak var loginTextField: FramedTextField!
@IBOutlet weak var passwordTextField: FramedTextField!
@IBOutlet weak var connectButton: UIButton!
let viewModel : LoginViewModel = LoginViewModel()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.login <~ loginTextField.rac_text
viewModel.password <~ passwordTextField.rac_text
connectButton.addTarget(self.viewModel.cocoaActionLogin, action: CocoaAction.selector, forControlEvents: .TouchUpInside)
self.viewModel.loginAction.events
.observeOn(UIScheduler())
.observeNext { (event) in
switch event {
case .Completed:
SVProgressHUD.dismiss()
self.connectionSuccessfull()
case .Failed(let error):
switch error {
case .MissingPasswordError :
SVProgressHUD.showErrorWithStatus(NSLocalizedString("Missing password", comment: "User password is missing"))
case .MissingLoginError :
SVProgressHUD.showErrorWithStatus(NSLocalizedString("Missing login", comment: "User login is missing"))
case .MoyaError(let error) :
SVProgressHUD.showErrorWithStatus(error.toString())
default :
SVProgressHUD.showErrorWithStatus(NSLocalizedString("Internal error, please try again later", comment: ""))
}
default:
break
}
}
}
}