Form validation made easy — iOS

Sujal Shrestha
4 min readDec 13, 2020

Let’s be honest, every project we work in has at-least one form- be it a login form, sign up and many more. It is crucial for us to validate the user inputs in the text fields.

Let’s make a generic way to easily validate text fields which can be used in multiple projects as well as can be easily tested.

You can check the example code and unit tests at my GitHub repo: https://github.com/sujalshrestha/FieldValidator

Let’s start with an enum and call it FieldType

enum FieldType {case emailcase passwordcase phonecase normal
var minLength: Int? {switch self {case .email, .password: return 8case .phone: return 10default: return nil}}
var maxLength: Int? {switch self {case .email, .password: return 50case .phone: return 10default: return nil}}var validationMessage: String? {switch self {case .email: return "Invalid Email"case .password: return "Password must have atleast 1 capital character, 1 number and minimum 8 characters"case .phone: return "Phone must be exact 10 digits"default: return nil}}}

As we can see, we have defined the types of fields (Email, Password, Phone), as well as rules (minimum length, maximum length). These can be extended according to project needs, and as all the rules can be defined in this enum, it can be easily modified later on as the project grows.

Now, let’s create a FieldValidator struct which will be used to check if the fields are valid or not.

struct FieldValidator {let fieldType: FieldTypeinit(fieldType: FieldType) {self.fieldType = fieldType}internal func isMinLength(text: String) -> Bool {guard let minCount = fieldType.minLength else { return true }return text.count >= minCount}internal func isMaxLength(text: String) -> Bool {guard let maxCount = fieldType.maxLength else { return true }return text.count <= maxCount}internal func isEmailValid(text: String) -> Bool {if fieldType == .email { return isValidEmail(text: text) }return true}internal func isPasswordValid(text: String) -> Bool {if fieldType != .password { return true }return checkPasswordValidity(password: text)}private func isValidEmail(text:String) -> Bool {let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx)return emailTest.evaluate(with: text)}private func checkPasswordValidity(password: String) -> Bool {let passwordValue = passwordlet capitalLetterRegEx  = ".*[A-Z]+.*"let capitalStringCheck = NSPredicate(format:"SELF MATCHES %@", capitalLetterRegEx)let capitalResult = capitalStringCheck.evaluate(with: passwordValue)let smallLetterRegEx  = ".*[a-z]+.*"let smallStringCheck = NSPredicate(format:"SELF MATCHES %@", smallLetterRegEx)let smallResult = smallStringCheck.evaluate(with: passwordValue)let numberRegEx  = ".*[0-9]+.*"let numberCheck = NSPredicate(format:"SELF MATCHES %@", numberRegEx)let numberResult = numberCheck.evaluate(with: passwordValue)return capitalResult && smallResult && numberResult}}

This struct FieldValidator takes the enum FieldType as parameter and has functions which returns boolean value based on the field type.

Now, lets create a generic UITextField class and name it MyTextField:

class MyTextField: UITextField {typealias fieldValidMessage = (Bool, String?)let fieldType: FieldTypelet validator: FieldValidatorvar isValid: ((fieldValidMessage) -> Void)?init(title: String, fieldType: FieldType) {self.fieldType = fieldTypevalidator = FieldValidator(fieldType: self.fieldType)super.init(frame: .zero)setupUI(title: title)observeEvents()}private func setupUI(title: String) {placeholder = titleborderStyle = .roundedRectdelegate = selfisSecureTextEntry = fieldType == .password}private func observeEvents() {addTarget(self, action: #selector(handleTextChange), for: .editingChanged)}@objc func handleTextChange(_ textField: UITextField) {let text                    = textField.text ?? ""let minimunLengthValidy     = validator.isMinLength(text: text)let maximumLengthValidity   = validator.isMaxLength(text: text)let emailValidity           = validator.isEmailValid(text: text)let passwordValidity        = validator.isPasswordValid(text: text)let isFieldValid            = (minimunLengthValidy && maximumLengthValidity && emailValidity && passwordValidity)isValid?((isFieldValid, isFieldValid ? nil : fieldType.validationMessage))}required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}}

As we can see, MyTextField is instantiated with the placeholder title and FieldType enum. handleTextChange will be called every-time an user enters text in our text field and will check if the respective field is valid or not (based on our rules setup in FieldType).

Likewise, I have created a closure: isValid which will return if the field is valid or not, as well as a validation message. For this callback, you can also use any communication pattern like delegates and protocol, RxSwift or Combine. For simplicity, I’ve used closure in this example.

Finally, let's see how we implement these in our ViewController.

let emailField = MyTextField(title: "Enter email", fieldType: .email)let passwordField = MyTextField(title: "Enter password", fieldType: .password)let phoneField = MyTextField(title: "Enter phone", fieldType: .phone)let optionalField = MyTextField(title: "Optional field", fieldType: .normal)private func observeEvents() {emailField.isValid = { [weak self] (fieldValidMessage) inguard let self = self else { return }self.emailMessage.text = fieldValidMessage.1 ?? ""self.emailMessage.isHidden = fieldValidMessage.0}passwordField.isValid = { [weak self] fieldValidMessage inguard let self = self else { return }self.passwordMessage.text = fieldValidMessage.1 ?? ""self.passwordMessage.isHidden = fieldValidMessage.0}phoneField.isValid = { [weak self] fieldValidMessage inguard let self = self else { return }self.phoneMessage.text = fieldValidMessage.1 ?? ""self.phoneMessage.isHidden = fieldValidMessage.0}}

We simply, call the closure and observe any changes to it. We can reflect the validation status in our UI, based on the value returned by the closure.

Now for a bonus part, we can easily test the Validator function to catch any unwanted bugs. Here’s an example:

func testEmailValid() {let validator = FieldValidator(fieldType: .email)let isValid = validator.isEmailValid(text: "suj@gmail.com")XCTAssertTrue(isValid)}func testEmailInValid() {let validator = FieldValidator(fieldType: .email)let isValid = validator.isEmailValid(text: "s@s")XCTAssertFalse(isValid)}

For the example code, please visit my GitHub repo: https://github.com/sujalshrestha/FieldValidator

--

--