Gui Gui - 2 months ago 9
iOS Question

Form validation in Swift

I have recently commenced working on a Swift (2) iOS project, and started to face circumstances where I have forms with many fields which need validating.

I have a predominantly .Net background, and with Binding and Annotations it is possible to implement form validation cleanly in a low maintenance manner, on large forms, with few lines of code.

Since embracing Swift, I have come across many examples detailing validation in various fashions, but everything I have encountered thus far seems very labour intensive and high maintenance, or the explanations focus on conducting the validation itself rather than connecting the validation process between the model and the view efficiently.

For example:



My current solution defines extensions which can be checked in a function, rule-by-rule, when validating field input, however I feel there must be a more scalable solution to this problem.

What approaches can be taken, when validating forms with potentially many inputs, that promote maintainability?

For example sake, we could discuss a hypothetical form with:


  • A textField requesting a number be entered (Int).

  • A textField requesting a decimal number be entered (Double).

  • A textField requesting a number be entered, matching specific rules (Eg, is prime) (Double).

  • A textField requesting a string be entered of some length kL.

  • A textField requesting a string be entered conforming to some custom rule.



Of course, I am not looking for an implementation that literally satisfies the list above, but instead a method or approach that is scaleable across these types of scenarios.

Answer

"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 cannot 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
}