A Functional Approach to UITextField Validation
Thibault Klein
Thibault Klein

A Functional Approach to UITextField Validation

Thibault Klein, Senior iOS Engineer

It is very common for an app to validate user input and provide instant feedback when something is typed incorrectly. In this post, I will walk through the implementation I worked on to validate a text field’s string. I decided to attempt a functional approach and found a solution that is reusable and scalable for every kind of validation.

Identifying the Functions

One thing that the functional programming paradigm urges us to do is to think how computation can be broken down into functions that reliably take an input and produce an output, without modifying state.

When I approached this problem, my first step was to identify what kind of data I wanted to deal with. I figured that the entry data would always be a String type and the result a Bool. So I’d write validation functions that would take a string as an argument and return true or false.

With that in mind, I wanted another function that would allow me to take an unlimited amount of validation functions, compute the result of all of them, and return the final result. I wrote a function that would take an array of functions with the same signature as the validation functions (String -> Bool); that function would return a Bool as the final result of evaluating all validation function.

For those more familiar with functional concepts, this is a take on using a “pipeline” – a sequence of operations that are applied in succession to, generally, produce a value.

Implementation

I decided to create a UITextField extension and came up with an implementation using map and reduce, two functions commonly used in functional programming:

extension UITextField {
    func validateField(functions: [String -> Bool]) -> Bool {
        return functions.map { f in f(self.text ?? "") }.reduce(true) { $0 && $1 }
    }
}

validateField iterates over the functions array and applies each function with the current UITextField text, reduces each result into the final result and returns it.

All I needed after was a bunch of validation functions taking a String as argument and returning a Bool:

func isPhoneNumberValid(text: String) -> Bool
func isZipCodeValid(text: String) -> Bool
func isStateValid(text: String) -> Bool
func isCVCValid(text: String) -> Bool

This functional approach gave me some consistency through all the validation functions, and thanks to that I found a way to use all of them the same way in a clean, short function.

Evaluation with Regular Expression

For the actual implementation of these validation functions, I created an evaluate function as part of a String extension that would take a regular expression as an argument and return a Bool based on the evaluation:

extension String {

    func evaluate(with condition: String) -> Bool {
        guard let range = range(of: condition, options: .regularExpression, range: nil, locale: nil) else {
            return false
        }

        return range.lowerBound == startIndex && range.upperBound == endIndex
    }

}

I could then use this evaluate function for each validation function:

func isPhoneNumberValid(text: String) -> Bool {
    let regexp = "^[0-9]{10}$"
    return text.evaluate(regexp)
}

func isZipCodeValid(text: String) -> Bool {
    let regexp = "^[0-9]{5}$"
    return text.evaluate(regexp)
}

func isStateValid(text: String) -> Bool {
    let regexp = "^[A-Z]{2}$"
    return text.evaluate(regexp)
}

func isCVCValid(text: String) -> Bool {
    let regexp = "^[0-9]{3,4}$"
    return text.evaluate(regexp)
}

func isEmailValid(text: String) -> Bool {
    let regexp = "[A-Z0-9a-z._]+@([\w\d]+[\.\w\d]*)"
    return text.evaluate(regexp)
}

And here is how I call my validation implementation on any text field:

textField.validateField([isStateValid])

The beauty is that any number of validation functions can be added to this list and validateField will work reliably. Moreover, it is trivial for other developers to read such a descriptive list of validations and understand what is required of the field.

Testing

Testing this code is also trivial:

func test_whenTextFieldIsValidState() {
    // Given
    let textField = UITextField()
    // When
    textField.text = "NY"
    // Then
    XCTAssertTrue(textField.isStateValid(textField.text!))
}
 
func test_whenTextFieldIsInvalidEmail_withUnderscoreCharacter() {
    // Given
    let textField = UITextField()
    // When
    textField.text = "thibault+test@_gmail.com"
    // Then
    XCTAssertFalse(textField.isEmailValid(textField.text!))
}

Taking it Further: Protocols

The implementation is working great, but the code is only available for UITextField class through the extension. It isn’t reusable for anything. I basically broke an important protocol-oriented maxim: if you want to create a class, create a protocol first.

This time, instead of creating a class extension, I will start with protocols:

protocol Validatable {
    associatedtype T
 
    func validate(_ functions: [T]) -> Bool
}
 
protocol Evaluatable {
    associatedtype T
 
    func evaluate(with condition: T) -> Bool
}

The evaluate and validate abilities are now available for anyone conforming to them. Note that I’m using associated types to make the validation and evaluation types generic.

Let’s now revisit the UITextField example using these protocols:

extension UITextField: Validatable {
 
    func validate(_ functions: [(String) -> Bool]) -> Bool {
        return functions.map { f in f(self.text ?? "") }.reduce(true) { $0 && $1 }
    }
}

We extend the UITextField class to conform to Validatable and the validation logic stays the same. The associated type is defined here as a String -> Bool function.

extension String: Evaluatable {
    func evaluate(with condition: String) -> Bool {
        guard let range = range(of: condition, options: .regularExpression, range: nil, locale: nil)     else {
            return false
        }
 
        return range.lowerBound == startIndex && range.upperBound == endIndex
    }
}

We extend the String class to conform to Evaluatable and implement the same logic as before. The associated type is defined here as a String type.

Now we can use the evaluation functions exactly the same way as before:

func isCVCValid(text: String) -> Bool {
    let regexp = "^[0-9]{3,4}$"
    return text.evaluate(with: regexp)
}
 
let cvcTextField = UITextField()
cvcTextField.text = "123"
cvcTextField.validate([isCVCValid])

Another Use for Our Protocols

Let’s see how using protocols can be beneficial for a complete different part of your code. Let’s take an example with a User model that you want to validate:

struct User {
    let firstName: String
    let lastName: String
    let age: Int
}

We can make User conform to our Validatable protocol:

extension User: Validatable {
    func validate(_ functions: [(User) -> Bool]) -> Bool {
        return functions.map { f in f(self) }.reduce(true) { $0 && $1 }
    }
}

The associated type is defined as a User -> Bool function because we want to take a user as a parameter and return a boolean value to validate it.

Let’s also define the evaluation functions we will use:

func isUserNameValid(user: User) -> Bool {
    let regexp = "[A-Za-z]+"
    return user.firstName.evaluate(with: regexp) && user.lastName.evaluate(with: regexp)
}
 
func isUserAdult(user: User) -> Bool {
    return user.age >= 18
}

Finally we can create a user and test the validation feature:

let user = User(firstName: "Thibault", lastName: "Klein", age: 25)
XCTAssertTrue(user.validate([isUserNameValid, isUserAdult]))

The validation and evaluation features are now available for another type that I defined. On top of that it still keeps the benefits of functional programming I described above. This example might not be the most useful one possible, but it gives us a sense that anything can be validated now that I’ve abstracted validation into protocols.

Conclusion

This implementation gives me a lot of options if I want to combine multiple validation functions for a text field – I simply create an extra validation function and apply it. Moreover, it provides a very descriptive way to understand what validations need to take place for a given text field. And, it is very easy to test.

Though many iOS developers are not familiar with functional concepts, it sometimes pays off to start by thinking through how to solve a small problem in a functional way. Creating functions with minimal responsibilities can make code clean and concise. It made my code easy to test and scale. On top of that this functional approach perfectly interoperates with my object-oriented code.I also definitely recommend thinking more towards protocol first the next time you want to introduce a new feature in your code, as it can make it more flexible, descriptive, and reusable in the future.

So far this functional solution has proven to be very efficient for the projects I’ve used it in, and I hope it will be useful for you as well! You’ll find a Xcode playground in this repository that has all the code I used in this article.