rjndra
rjndra

Reputation: 121

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.

Upvotes: 2

Views: 3275

Answers (2)

MeXx
MeXx

Reputation: 3357

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.

Upvotes: 10

rjndra
rjndra

Reputation: 121

Waiting for answer to be supported or for better answer.

I tried to solve myself as stated in question.

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!

  // Singals Start
  var nameSignal:SignalProducer<Bool, NoError>!
  var phoneSignal:SignalProducer<Bool, NoError>!
  var emailSignal:SignalProducer<Bool, NoError>!
  var locationSignal:SignalProducer<Bool, NoError>!
  var commentSignal:SignalProducer<Bool, NoError>!
  // Signals End

  private var viewModel = ComtactViewModel()

  override func viewDidLoad() {
    super.viewDidLoad()

    checkLocationAuthorizationStatus()
    setupBindings()
  }

  func setupBindings() {

    //binding to view model to UI
    self.textFieldName.reactive.text <~ self.viewModel.name
    self.textFieldPhoneOL.reactive.text <~ self.viewModel.phoneNumber
    self.textFieldEmailOL.reactive.text <~ self.viewModel.emailAddress
    self.textFieldLocationOL.reactive.text <~ self.viewModel.location
    self.textViewComent.reactive.text <~ self.viewModel.comment

  }
  // MARK: - submit button action
  @IBAction func btnSubmitAction(_ sender: Any) {
    self.btnSubmitOL.isUserInteractionEnabled = false
    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.viewModel.name.value = ""
      self.viewModel.phoneNumber.value = ""
      self.viewModel.emailAddress.value = ""
      self.viewModel.location.value = ""
      self.viewModel.comment.value = ""

      Utilities.showAlert(alertTitle: "sucess", alertMessage: response.message!, viewController: self, didTabOkButton: {
        self.btnSubmitOL.backgroundColor = UIColor.gray
        self.btnSubmitOL.isUserInteractionEnabled = false

      }, didTabOnCancelButton: nil)

    })

  }

  // MARK: - validation textfield

  func formValidation(){

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

    // Create signals
    // Signals for ViewModels for crossCheck
    self.nameSignal = self.viewModel.name.producer.map{ $0.characters.count >= 3 }.producer
    self.phoneSignal = self.viewModel.phoneNumber.producer.map{ $0.characters.count >= 8 }.producer
    self.emailSignal = self.viewModel.emailAddress.producer.map{ $0.isEmail }.producer
    self.locationSignal = self.viewModel.location.producer.map{ $0.characters.count >= 3 }.producer
    self.commentSignal = self.viewModel.comment.producer.map{ $0.characters.count >= 5 }.producer

    // Signals for TextFields
    self.textFieldName.reactive.continuousTextValues.skipNil()
        .observeValues { self.viewModel.name.value = $0 }
    self.textFieldPhoneOL.reactive.continuousTextValues.skipNil()
        .observeValues { self.viewModel.phoneNumber.value = $0 }
    self.textFieldEmailOL.reactive.continuousTextValues.skipNil()
        .observeValues { self.viewModel.emailAddress.value = $0 }
    self.textFieldLocationOL.reactive.continuousTextValues.skipNil()
        .observeValues{ self.viewModel.location.value = $0 }
    self.textViewComent.reactive.continuousTextValues.skipNil()
        .observeValues { self.viewModel.comment.value = $0 }

    // Observe TextFields Singals for Changing UI
    self.nameSignal.startWithValues { value in
      self.textFieldName.textColor = value ? UIColor.appRedColor() : UIColor.black
      self.textFieldName.floatingLabel.text = value ? "name".localize : "Name must be greater than 4 characters".localize
    }

    self.phoneSignal.startWithValues { value in
      self.textFieldPhoneOL.textColor = value ? UIColor.appRedColor() : UIColor.black
      self.textFieldPhoneOL.floatingLabel.text = value ? "phone".localize : "Phone must be greater than 7 characters".localize
    }

    self.emailSignal.startWithValues { value in
      self.textFieldEmailOL.textColor = value ? UIColor.appRedColor() : UIColor.black
      self.textFieldEmailOL.floatingLabel.text = value ? "email".localize : "Email must be of type [email protected]".localize
    }

    self.locationSignal.startWithValues { value in
      self.textFieldLocationOL.textColor = value ? UIColor.appRedColor() : UIColor.black
      self.textFieldLocationOL.floatingLabel.text = value ? "location".localize : "Loation must be greater than 4 characters".localize
    }

    self.commentSignal.startWithValues { value in
      self.textViewComent.textColor = value ? UIColor.appRedColor() : UIColor.black
    }


    let formValidationViewModelSignal = self.nameSignal.combineLatest(with: self.phoneSignal).combineLatest(with: self.emailSignal).combineLatest(with: self.locationSignal).combineLatest(with: self.commentSignal).map {
      $0.0.0.0 && $0.0.0.1 &&  $0.0.1 && $0.1 && $1
    }


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

ContactView Model Class

import Foundation
import ReactiveSwift

class ContactViewModel {

    var name = MutableProperty("")
    var phoneNumber = MutableProperty("")
    var emailAddress = MutableProperty("")
    var location = MutableProperty("")
    var comment = MutableProperty("")

}

Upvotes: 0

Related Questions