Search code examples
swiftmvvmfunctional-programmingreactive-cocoabidirectional

Signals and Observer(Reactive Swift) for form validation not working as expected


I am doing form validation using reactive swift. But I faced issue on resetting value & signal value.

When I fill all the textfield correctly as directed by validation rule all signals(textfield continuoustextvalues) produce true value, which will allow me to send form data. I reset values of textfield after completion of form submission. After that I send false value to all signal Observer. But when I start filling textfield it will get previous true signal and allow me to send data without any validation rule applied. that means I can't reset signal value

Any help would be really appreciated.

My Problem:

import UIKit
import ReactiveSwift
import Result

class ContactVC: BaseViewController {

    @IBOutlet weak var textFieldName: JVFloatLabeledTextField!
    @IBOutlet weak var textFieldPhoneOL: JVFloatLabeledTextField!
    @IBOutlet weak var textViewComent: UITextView!
    @IBOutlet weak var textFieldLocationOL: JVFloatLabeledTextField!
    @IBOutlet weak var textFieldEmailOL: JVFloatLabeledTextField!
    @IBOutlet weak var btnSubmitOL: PGSpringAnimation!

    var (nameValidationSignal, nameValidationObserver) = Signal<Bool, NoError>.pipe()
    var (phoneValidationSignal, phoneValidationObserver) = Signal<Bool, NoError>.pipe()
    var (emailValidationSignal, emailValidationObserver) = Signal<Bool, NoError>.pipe()
    var (locationValidationSignal, locationValidationObserver) = Signal<Bool, NoError>.pipe()
    var (commentValidationSignal, commentValidationObserver) = Signal<Bool, NoError>.pipe()


    override func viewDidLoad() {
        super.viewDidLoad()

    }


    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        self.formValidation()
    }

    // MARK: - submit button action
    @IBAction func btnSubmitAction(_ sender: Any) {

        let params  = ["name":textFieldName.text!,"email":textFieldEmailOL.text!,"location":textFieldLocationOL.text!,"message":textViewComent.text!,"phone":textFieldPhoneOL.text!]

        APIManager(urlString:enumUrl.ContactAdmin.mainURL(),parameters:params as [String : AnyObject]?,method: .post).handleResponse(viewController: self, progressMessage: "downloading", completionHandler:  { (response : AllResponse) in

            self.nameValidationObserver.send(value: false)
            self.emailValidationObserver.send(value: false)
            self.phoneValidationObserver.send(value: false)
            self.locationValidationObserver.send(value: false)
            self.commentValidationObserver.send(value: false)

            self.btnSubmitOL.backgroundColor = UIColor.gray
            self.btnSubmitOL.isUserInteractionEnabled = false

        })

    }
    // MARK: - validation textfield

    func formValidation(){

        self.btnSubmitOL.backgroundColor = UIColor.gray
        self.btnSubmitOL.isUserInteractionEnabled = false

        // Create signals

        // Signals for TextFields
        self.nameValidationSignal = self.textFieldName.reactive.continuousTextValues
            .map{ ($0?.characters.count ?? 0) >= 3 }
        self.phoneValidationSignal = self.textFieldPhoneOL.reactive.continuousTextValues
            .map{ ($0?.characters.count ?? 0 ) >= 8 }
        self.emailValidationSignal = self.textFieldEmailOL.reactive.continuousTextValues
            .map{ $0?.isEmail ??  false }
        self.locationValidationSignal = self.textFieldLocationOL.reactive.continuousTextValues
            .map{ ($0?.characters.count ?? 0) >= 3 }
        self.commentValidationSignal = self.textViewComent.reactive.continuousTextValues
            .map{ ($0?.characters.count ?? 0) >= 5 }

        // Observe TextFields Singals for Changing UI
        self.nameValidationSignal.observeValues { value in
            self.textFieldName.floatingLabelActiveTextColor = value ? UIColor.red : UIColor.black
            self.textFieldName.floatingLabel.text = value ? "name".localize : "Name must be greater than 4 characters".localize
        }

        self.phoneValidationSignal.observeValues { value in
            self.textFieldPhoneOL.floatingLabelActiveTextColor = value ? UIColor.red : UIColor.black
            self.textFieldPhoneOL.floatingLabel.text = value ? "phone".localize : "Phone must be greater than 7 characters".localize
        }

        self.emailValidationSignal.observeValues { value in
            self.textFieldEmailOL.floatingLabelActiveTextColor = value ? UIColor.red : UIColor.black
            self.textFieldEmailOL.floatingLabel.text = value ? "email".localize : "Email must be of type [email protected]".localize
        }

        self.locationValidationSignal.observeValues { value in
            self.textFieldLocationOL.floatingLabelActiveTextColor = value ? UIColor.red : UIColor.black
            self.textFieldLocationOL.floatingLabel.text = value ? "location".localize : "Loation must be greater than 4 characters".localize
        }

        self.commentValidationSignal.observeValues { value in
            self.textViewComent.textColor = value ? UIColor.red : UIColor.black
        }


        let formValidationSignal = nameValidationSignal.combineLatest(with: phoneValidationSignal).combineLatest(with: emailValidationSignal).combineLatest(with: locationValidationSignal).combineLatest(with: commentValidationSignal)
            .map {
                $0.0.0.0 && $0.0.0.1 &&  $0.0.1 && $0.1 && $1
        }


        formValidationSignal.observeValues {
                self.btnSubmitOL.isUserInteractionEnabled = $0
                self.btnSubmitOL.backgroundColor = $0 ? UIColor.appRedColor() : UIColor.gray
        }
    }

}

I have made solution to this problem but I don't think it's perfect way and the reactive is not way I have done to solve. I am waiting for perfect or most accepted Solution. Any help or answer is really Appreciated.


Solution

  • Here is my take on this with a more idiomatic approach (simplified to only two inputs for the sake of the example).

    First, there is a ViewModel that has MutablePropertys to hold the input values. You could initialize these values to anything else than nil if you want other initial values for the inputs.

    The ViewModel als has a properties for the validation of the inputs. Property.map is used to infer valid values from the input. Btw, you can use Signal.combineLatest(signal1, signal2, signal3, ...) instead of signal1.combineLatest(with: signal2).combineLatest(with: signal3)...

    Finally, there's an Action that performs the submission. In the ViewController, we can bind this Action to the button. The Action sends an empty string each time it is performed. The .values signal of the action is used to reset the inputs after the action is performed. If the submission could produce an error, you should handle this accordingly.

    class ViewModel {
        let username = MutableProperty<String?>(nil)
        let address = MutableProperty<String?>(nil)
        let usernameValid: Property<Bool>
        let addressValid: Property<Bool>
        let valid: Property<Bool>
        let submit: Action<(String?, String?), String, NoError>
    
        init() {
    
            self.usernameValid = username.map {
                return ($0 ?? "").characters.count > 0
            }
            self.addressValid = address.map {
                return ($0 ?? "").characters.count > 0
            }
    
            self.valid = Property.combineLatest(self.usernameValid, self.addressValid).map { (usernameValid, addressValid) in
                return usernameValid && addressValid
            }
            self.submit = Action(enabledIf: self.valid) { input in
                print("Submit with username \(input.0) and address \(input.1)")
                return SignalProducer<String, NoError>(value: "")
            }
    
            self.username <~ self.submit.values
            self.address <~ self.submit.values
        }
    }
    

    Then there's the setup in the ViewController:

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        self.username.reactive.text <~ self.viewModel.username
        self.address.reactive.text <~ self.viewModel.address
    
        self.viewModel.username <~ self.username.reactive.continuousTextValues
        self.viewModel.address <~ self.address.reactive.continuousTextValues
    
        self.submit.reactive.pressed = CocoaAction(self.viewModel.submit) { [weak self] (button) -> (String?, String?) in
            return (self?.username.text, self?.address.text)
        }
    }
    

    First, the MutablePropertys of the ViewModel are bound to the UITextFields. This way, the text fields are not only initialised to the initial values of the properties in the ViewModel, but also they are updated if the properties in the ViewModel are updated - this way you can reset them when the submit action is performed.

    Then, the continuousTextValues of the UITextFields are bound to the properties of the ViewModel. Since continuousTextValues does not fire if the text is set programatically, only if it is set by the User, this does not create a loop.

    Finally, CocoaAction is used to bind the submit action to the button's pressed Action. The inputTransformer function is used to send the current values of the inputs each time the button is pressed.

    You can also subscribe to the individual usernameValid / addressValid properties of the viewModel to set display validation errors to the user here.