Search code examples
iosswiftreactive-cocoareactive-swiftphonenumberkit

Enable/Disable button with validating phone number entered in a textfield


I'm very new to ReactiveSwift and MVVM as a whole. I'm trying to validate phone numbers entered into a textfield and enable/disable a button depending on the validation result.

In the app, there is a textfield and a UIButton button called Submit. For phone number validating, I'm using an open source library called [PhoneNumberKit][1]. It also provides a UITextField subclass which formats the user input.

I mashed together a solution that looks like this.

class ViewController: UIViewController {
    @IBOutlet weak var textField: PhoneNumberTextField!
    @IBOutlet weak var submitButton: UIButton!


    override func viewDidLoad() {
        super.viewDidLoad()
        textField.becomeFirstResponder()
        submitButton.isEnabled = false

        let textValuesSignal = textField.reactive.continuousTextValues
        SignalProducer(textValuesSignal).start { result in
            switch result {
            case .value(let value):
                print(value)
                self.submitButton.isEnabled = self.isPhoneNumberValid(value)
            case .failed(let error):
                print(error)
                self.submitButton.isEnabled = false
            case .interrupted:
                print("inturrupted")
                self.submitButton.isEnabled = false
            case .completed:
                print("completed")
            }
        }
    }

    func isPhoneNumberValid(_ phoneNumberString: String) -> Bool {
        do {
            let phoneNumber = try PhoneNumberKit().parse(phoneNumberString)
            let formattedPhoneNumber = PhoneNumberKit().format(phoneNumber, toType: .e164)
            print("Phone number is valid: \(formattedPhoneNumber)")
            return true
        } catch let error {
            print("Invalid phone number: \(error)")
            return false
        }
    }
}

This does the job but not very elegantly. Also there is a significant lag between user input and the UI changing.

Another thing is my above solution doesn't conform to MVVM. I gave it another go.

class ViewController: UIViewController {
    @IBOutlet weak var textField: PhoneNumberTextField!
    @IBOutlet weak var submitButton: UIButton!

    private let viewModel = ViewModel()


    override func viewDidLoad() {
        super.viewDidLoad()
        textField.becomeFirstResponder()

        submitButton.reactive.isEnabled <~ viewModel.isPhoneNumberValid

        viewModel.phoneNumber <~ textField.reactive.continuousTextValues
    }
}

class ViewModel {
    let phoneNumber = MutableProperty("")
    let isPhoneNumberValid = MutableProperty(false)

    init() {
        isPhoneNumberValid = phoneNumber.producer.map { self.validatePhoneNumber($0) } // Cannot assign value of type 'SignalProducer<Bool, NoError>' to type 'MutableProperty<Bool>'
    }

    private func validatePhoneNumber(_ phoneNumberString: String) -> Bool {
        do {
            let phoneNumber = try PhoneNumberKit().parse(phoneNumberString)
            let formattedPhoneNumber = PhoneNumberKit().format(phoneNumber, toType: .e164)
            print("Phone number is valid: \(formattedPhoneNumber)")
            return true
        } catch let error {
            print("Invalid phone number: \(error)")
            return false
        }
    }
}

I'm getting the below error in the initializer when I'm assigning the result from the validatePhoneNumber function's to the isPhoneNumberValid property.

Cannot assign value of type 'SignalProducer' to type 'MutableProperty'

I can't figure out how to hook up the phone number validation part with the submit button's isEnabled property and the tap action properly.

Demo project


Solution

  • Try setting the property in init and mapping the property itself rather than a producer:

    let isPhoneNumberValid: Property<Bool>
    
    init() {
        isPhoneNumberValid = phoneNumber.map { ViewModel.validatePhoneNumber($0) }
    }
    

    You’ll have to make validatePhoneNumber a static method because self won’t be available yet.

    This is a more typical reactive way of doing this because you define one property completely in terms of another one during the view model’s initialization.