Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rx Swift : Complex TextFields Validation

I'm new to RxSwift and all example I found are handling simple cases.

I'm trying to do form validation for my textfields. My custom TextField class has a method isValid() and a regexp. The isValid return is based on the regexp attribute.

So far, I have written the following :

let valids = [mLastName, mFirstName, mEmailField].map {
    $0.rx.text.map {
        text -> Bool in
        // I want more complex logic here
        // Like return field.isValid()
        return text!.characters.count > 0
    }
}    

let _ = Observable.combineLatest(valids) { iterator -> Bool in
    return iterator.reduce(true, { $0 && $1 })
}.subscribe(onNext: { allValid in
    ///update button according to AllValid
})

Does anyone know how to update the code to base the first Observable<Bool> be based on my isValid() method instead of text!.characters.count

like image 909
CZ54 Avatar asked Mar 17 '17 14:03

CZ54


2 Answers

There are probably many ways to do that.

You can use filter to transform rx.text Observable in your custom TextField class:

var isTextValid: Observable<Bool> {
    return rx.text.filter { _ in 
        return self.isValid()
    }
}

then you can combine isTextValid from all text fields with combineLatest.


You can also extract the validation logic from the custom text field (maybe you don't even need a custom text field at all).

Benefits:

  • validation could be easier to unit test
  • you can easily reuse validation in different places in your app (e.g. for UITextView if you ever use it).

The draft of a validator class:

class TextValidator {
    var input: Observable<String>
    var regex: NSRegularExpression

    init(input: Observable<String>, regex: NSRegularExpression) {
       self.input = input
       self.regex = regex
    }

    func validate() -> Observable<Bool> {
        return input.map { text in 
             //return true if regex matches the text
        }
    }       
}

Then you can use it as follows:

let mailValidator = TextValidator(input: mLastName.rx.text, regex: /* actual regex*/)
let firstNameValidator = TextValidator(input: mFirstName.rx.text, regex: ...)



let _ = Observable.combineLatest(mailValidator.validate(), firstName.validate(), ...) 
// and so on

Now if you want to write unit tests for the validators (which you probably should do), you can simply pass Observable.just("Some value") as input to TextValidator and verify what Observable returned by validate() does.

like image 162
Michał Ciuba Avatar answered Sep 28 '22 16:09

Michał Ciuba


I found the answer myself. The problem was in the first map, I shouldn't use anonymous parameter.

See :

let valids = [mLastName, mFirstName, mEmailField].map { field in
    field.rx.text.map({ _ in return field.isValid() })
}

_ = Observable.combineLatest(valids) { iterator -> Bool in
    return iterator.reduce(true, { return $0 && $1 })
}.bindTo(self.mValidateButton.rx.isEnabled)
like image 26
CZ54 Avatar answered Sep 28 '22 14:09

CZ54