Form validation in Swift

"Ugh, forms"

-Sir Albert Einstein

Yes, building a scaleable form in iOS can be a difficult and monotonous job. Which is why I have a base class called a FormViewController that exposes a few common validation methods and a few methods that you can use to add customised validation.

Now, the following code could be very long, and I am not going to explain each line. Do revert in the form of comments, if you have any doubts.

import UIKit

typealias TextFieldPredicate = ( (String) -> (Bool) )

class FormViewController : UIViewController {

    var activeTextField : UITextField!

    private var mandatoryFields  = [UITextField]()
    private var emptyErrorMessages = [String]()

    private var emailFields = [UITextField]()
    private var emailErrorMessages = [String]()

    private var specialValidationFields = [UITextField]()
    private var specialValidationMethods = [TextFieldPredicate]()
    private var specialValidationErrorMessages = [String]()

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        registerForNotifications()
    }

    override func viewWillDisappear(animated: Bool) {
        super.viewWillDisappear(animated)
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


    private func registerForNotifications() {
        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(FormViewController.keyboardWillShow(_:)), name:UIKeyboardWillShowNotification, object: nil);
        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(FormViewController.keyboardWillHide(_:)), name:UIKeyboardWillHideNotification, object: nil);
    }

    func keyboardWillShow(notification:NSNotification?) {
        let keyboardSize = notification?.userInfo![UIKeyboardFrameBeginUserInfoKey]!.CGRectValue.size
        self.view.frame.origin.y = 0
        let keyboardYPosition = self.view.frame.size.height - keyboardSize!.height
        if keyboardYPosition < self.activeTextField!.frame.origin.y {
            UIView.animateWithDuration(GlobalConstants.AnimationTimes.SHORT) { () -> Void in
                self.view.frame.origin.y = self.view.frame.origin.y - keyboardSize!.height + 30
            }
        }
    }

    func keyboardWillHide(notification:NSNotification?) {
        UIView.animateWithDuration(GlobalConstants.AnimationTimes.SHORT) { () -> Void in
            self.view.frame.origin.y = 0
        }
    }

    func validateEmailForFields(emailTextFields:[UITextField]) -> [Bool] {
        var validatedBits = [Bool]()
        for emailTextField in emailTextFields {
            if let text = emailTextField.text where !text.isValidEmail() {
                emailTextField.shakeViewForTimes(WelcomeViewController.ERROR_SHAKE_COUNT)
                validatedBits.append(false)
            } else {
                validatedBits.append(true)
            }
        }
        return validatedBits
    }

    func validateSpecialTextFields(specialTextFields:[UITextField]) -> [Bool] {
        var validatedBits = [Bool]()
        for specialTextField in specialTextFields {
            let specialValidationMethod = self.specialValidationMethods[ specialValidationFields.indexOf(specialTextField)!]
            validatedBits.append(specialValidationMethod(specialTextField.text!))
        }
        return validatedBits
    }

    func validateEmptyFields(textFields : [UITextField]) -> [Bool] {
        var validatedBits = [Bool]()
        for textField in textFields {
            if let text = textField.text where text.isEmpty {
                textField.shakeViewForTimes(WelcomeViewController.ERROR_SHAKE_COUNT)
                validatedBits.append(false)
            } else {
                validatedBits.append(true)
            }
        }
        return validatedBits
    }

    func addMandatoryField(textField : UITextField, message : String) {
        self.mandatoryFields.append(textField)
        self.emptyErrorMessages.append(message)
    }

    func addEmailField(textField : UITextField , message : String) {
        textField.keyboardType = .EmailAddress
        self.emailFields.append(textField)
        self.emailErrorMessages.append(message)
    }

    func addSpecialValidationField(textField : UITextField , message : String, textFieldPredicate : TextFieldPredicate) {
        self.specialValidationErrorMessages.append(message)
        self.specialValidationMethods.append(textFieldPredicate)
        self.specialValidationFields.append(textField)
    }

    func errorMessageForEmptyTextField(textField : UITextField) throws -> String  {
        if self.mandatoryFields.contains(textField) {
            return self.emptyErrorMessages[self.mandatoryFields.indexOf(textField)!]
        } else {
            throw ValidationError.NonMandatoryTextField
        }
    }

    func errorMessageForMultipleEmptyErrors() -> String {
        return "Fields cannot be empty"
    }

    func errorMessageForMutipleEmailError() -> String {
        return "Invalid email addresses"
    }

    @IBAction func didTapFinishButton(sender:AnyObject?) {
        if let errorMessage = self.errorMessageAfterPerformingValidation() {
            self.showVisualFeedbackWithErrorMessage(errorMessage)
            return
        }
        self.didCompleteValidationSuccessfully()
    }

    func showVisualFeedbackWithErrorMessage(errorMessage : String) {
        fatalError("Implement this method")
    }

    func didCompleteValidationSuccessfully() {

    }

    func errorMessageAfterPerformingValidation() -> String? {
        if let errorMessage = self.errorMessageAfterPerformingEmptyValidations() {
            return errorMessage
        }
        if let errorMessage = self.errorMessageAfterPerformingEmailValidations() {
            return errorMessage
        }
        if let errorMessage = self.errorMessageAfterPerformingSpecialValidations() {
            return errorMessage
        }
        return nil
    }

    private func errorMessageAfterPerformingEmptyValidations() -> String? {
        let emptyValidationBits = self.performEmptyValidations()
        var index = 0
        var errorCount = 0
        var errorMessage : String?
        for validation in emptyValidationBits {
            if !validation {
                errorMessage = self.emptyErrorMessages[index]
                errorCount += 1
            }
            if errorCount > 1 {
                return self.errorMessageForMultipleEmptyErrors()
            }
            index = index + 1
        }
        return errorMessage
    }

    private func errorMessageAfterPerformingEmailValidations() -> String? {
        let emptyValidationBits = self.performEmailValidations()
        var index = 0
        var errorCount = 0
        var errorMessage : String?
        for validation in emptyValidationBits {
            if !validation {
                errorMessage = self.emailErrorMessages[index]
                errorCount += 1
            }
            if errorCount > 1 {
                return self.errorMessageForMutipleEmailError()
            }
            index = index + 1
        }
        return errorMessage
    }

    private func errorMessageAfterPerformingSpecialValidations() -> String? {
        let emptyValidationBits = self.performSpecialValidations()
        var index = 0
        for validation in emptyValidationBits {
            if !validation {
                return self.specialValidationErrorMessages[index]
            }
            index = index + 1
        }
        return nil
    }

    func performEqualValidationsForTextField(textField : UITextField, anotherTextField : UITextField) -> Bool {
        return textField.text! == anotherTextField.text!
    }


    private func performEmptyValidations() -> [Bool] {
        return validateEmptyFields(self.mandatoryFields)
    }
    private func performEmailValidations() -> [Bool] {
        return validateEmailForFields(self.emailFields)
    }
    private func performSpecialValidations() -> [Bool] {
        return validateSpecialTextFields(self.specialValidationFields)
    }


}

extension FormViewController : UITextFieldDelegate {
    func textFieldDidBeginEditing(textField: UITextField) {
        self.activeTextField = textField
    }
    func textFieldDidEndEditing(textField: UITextField) {
        self.activeTextField = nil
    }
}

enum ValidationError : ErrorType {
    case NonMandatoryTextField
}

Another option that seems good so far is SwiftValidator. It is an active project and only took a few minutes for me to setup in my project.

https://github.com/jpotts18/SwiftValidator