How to use guard clause in Swift and let the code more clean

Viewed 144

I have the following code in my project:

//code used in my NEXT button

    let errorMessage:String = validateAllFields()
    if ( errorMessage != "" ) {
        AlertActions.showBasicAlert(erroParaExibir: errorMessage, currentView: self)
        return
    }
    ...more code to be executed


//code to validate if all fields are empty

func validateAllFields() -> String {        
    var errorMessage = ""
    if( nomeAnimalTextField.text == ""){
        errorMessage = "Preencha o nome do animal"
    } else if( microchipAnimalTextField.text == ""){
        errorMessage = "Preencha o microchip do animal"
    } else if( microchipAnimalTextField.text!.trim().count < 15){
        errorMessage = "Microchip do animal deve possuir 15 posições númericas"
    } else if( mesNascimentoTextField.text == ""){
        errorMessage = "Preencha o mês do nascimento"
    } else if( anoNascimentoTextField.text == ""){
        errorMessage = "Preencha o ano de nascimento"
    }
    return errorMessage
}

The code below is my guard clause

if ( errorMessage != "" ) {
    AlertActions.showBasicAlert(erroParaExibir: errorMessage, currentView: self)
    return
}

The return is protecting the rest of the code to not be executed if the condition fails.

I want to know how can I use only the validateAllFields() and insert the if condition with the "return" part of the code inside this function, is that possible?

2 Answers

I like John Montgomery's answer, but it is also very helpful to let errors be Errors rather than Strings. Then you can use Swift's error handling system. Rather than returning a String, throw:

struct ValidationError: Error {
    var localizedDescription: String
    init(_ message: String) { self.localizedDescription = message }
}

func validateAllFields() throws {
    if( nomeAnimalTextField.text == "") { throw ValidationError("Preencha o nome do animal") }
    if( microchipAnimalTextField.text == ""){ throw ValidationError("Preencha o microchip do animal") }
    if( microchipAnimalTextField.text!.trim().count < 15) {
        throw ValidationError("Microchip do animal deve possuir 15 posições númericas")
    }
    if( mesNascimentoTextField.text == "") { throw ValidationError("Preencha o mês do nascimento") }
    if( anoNascimentoTextField.text == "") { throw ValidationError("Preencha o ano de nascimento") }
}

And then when you want check this, use do/catch:

do {
    try validateAllFields()

    //    ...more code to be executed
} catch {
    AlertActions.showBasicAlert(erroParaExibir: error.localizedDescription, 
                                currentView: self)
}

Or you can move the catch up to the top and add a return if nothing else in the code can generate errors. (Or you can let the errors bubble up further by marking this method as throws.)

I would also suggest that showBasicAlert accept an Error rather than just a String.

The nice thing about using throws here is that it's flexible to more complex problems. For example, you could pull out some logic:

func validateNonEmpty(_ field: UITextField, or message: String) throws {
    if field.text == "" { throw ValidationError(message) }
}

func validateField(_ field: UITextField, atLeastLength minLength: Int, or message: String) throws {
    if field.text!.trim().count < minLength { throw ValidationError(message) }
}

And then write the validator as:

func validateAllFields() throws {
    try validateNonEmpty(nomeAnimalTextField, or: "Preencha o nome do animal")
    try validateNonEmpty(microchipAnimalTextField, or: "Preencha o microchip do animal")
    try validateField(microchipAnimalTextField, atLeastLength: 15,
                      or: "Microchip do animal deve possuir 15 posições númericas")
    try validateNonEmpty(mesNascimentoTextField, or: "Preencha o mês do nascimento")
    try validateNonEmpty(anoNascimentoTextField, or: "Preencha o ano de nascimento")
}

I'm not sure this is possible with a guard, at least not in a single step. The easiest way would be to change your validator to return an optional and use nil instead of an empty string as the default value:

func validateAllFields() -> String? {        
    var errorMessage: String?
    // rest of code is the same
    return errorMessage
}

Then use if let to test it:

if let errorMessage = validateAllFields() {
    AlertActions.showBasicAlert(erroParaExibir: errorMessage, currentView: self)
    return
}
Related