用于表单验证的信号和观察者(Reactive Swift)未按预期工作

时间:2017-01-17 06:32:10

标签: swift mvvm functional-programming reactive-cocoa bidirectional

我正在使用反应式swift进行表单验证。但我面临重置价值和问题的问题。信号值。

当我按照验证规则的指示正确填写所有文本字段时,所有信号(textfield continuoustextvalues)都会产生真值,这将允许我发送表单数据。完成表单提交后,我重置了textfield的值。之后,我向所有信号Observer发送false值。但是当我开始填充文本字段时,它将获得以前的真实信号,并允许我在不应用任何验证规则的情况下发送数据。这意味着我无法重置信号值

任何帮助都会非常感激。

我的问题:

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 example@test.com".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
        }
    }

}

我已经解决了这个问题,但我不认为这是完美的方式,并且反应不是我做的解决方法。 我正在等待完美或最受欢迎的解决方案。 任何帮助或答案都是真正的赞赏。

2 个答案:

答案 0 :(得分:8)

以下是我对此的看法,采用更惯用的方法(为了示例,简化为仅两个输入)。

首先,有一个ViewModel具有MutableProperty来保存输入值。如果您想要输入的其他初始值,则可以将这些值初始化为nil以外的任何值。

ViewModel als具有验证输入的属性。 Property.map用于从输入中推断有效值。顺便说一下,您可以使用Signal.combineLatest(signal1, signal2, signal3, ...)代替signal1.combineLatest(with: signal2).combineLatest(with: signal3)...

最后,有Action执行提交。在ViewController中,我们可以将此Action绑定到按钮。 Action每次执行时都会发送一个空字符串。操作的.values信号用于在执行操作后重置输入。 如果提交可能产生错误,您应该相应地处理。

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

然后是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)
    }
}

首先,ViewModel的MutableProperty绑定到UITextField。这样,文本字段不仅初始化为ViewModel中属性的初始值,而且如果更新了ViewModel中的属性,它们也会更新 - 这样,您可以在执行提交操作时重置它们。

然后,continuousTextValues的{​​{1}}绑定到ViewModel的属性。如果文本是以编程方式设置的,UITextFields不会触发,只有当用户设置时,才会创建循环。

最后,continuousTextValues用于将CocoaAction操作绑定到按钮的submit操作。 pressed函数用于在每次按下按钮时发送输入的当前值。

您还可以订阅viewModel的各个inputTransformer / usernameValid属性,以便在此处向用户设置显示验证错误。

答案 1 :(得分:0)

等待答案得到支持或获得更好的答案。

我试图解决自己的问题。

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 example@test.com".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("")

}