Search code examples
iosswiftmvvmreactive-cocoa

ReactiveCocoa 4, correctly send my HTTP request based on a UI event and a validation


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


Solution

  • 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
                }
    
            }
        }
    }