Validation and error checking

A full-featured and robust app needs to be prepared for thing to go wrong. One of the most common sources of trouble in an app is user input error. Any time an app takes inputs from a user the app must be prepared for things to go wrong. In these notes I will show some programming techniques designed to handle two of the most common problems related to user input: users forgetting to provide an input and users providing an incorrect input.

In software development validation is the process of checking user inputs for missing inputs or inputs that are improperly formatted. Error checking is the more generic process of checking for errors and responding to them gracefully when they occur.

Making the Online Directory app more robust

The main example I am going to be working with today is the online directory app. In particular, I am going to concentrate on the login page and things that can go wrong there.

Using the Swift guard

The Swift language contains a number of constructs specifically designed for error checking. One of these is the guard statement. Here is an example of some code that makes use of this construct. Below is the code for the action method for the log in button in the Directory app:

@IBAction func login(_ source : UIButton) {
    if let name = user!.text, let pwd = password!.text {
        guard !name.isEmpty else {
            showAlert(message: "You must provide a user name")
            return
        }
        guard !pwd.isEmpty else {
            showAlert(message: "You must provide a password")
            return
        }
        directory.user.name = name
        directory.user.password = pwd
        directory.getKey(success:{self.goToMain()},failure:{self.showAlert(message: "Login failed")})
    }
}

A guard statement is like an if statement in that it contains a test expression after the keyword guard. If the test expression evaluates to false, the guard statement will execute the code in the else branch. guard statements are used to test for critical failures: these are failures that are so bad that we have to give up and stop what we are doing. To enforce this, the else branch in a guard statement has to end in a return statement or some other construct that forces us to give up on doing any further work.

Another feature of the guard statement is that if the guard statement introduces a new variable that variable will stay in scope after the guard. To show how this works, here is the last example rewritten slightly to use this feature:

@IBAction func login(_ source : UIButton) {
    guard let name = user!.text, !name.isEmpty else {
            showAlert(message: "You must provide a user name")
            return
        }
    guard let pwd = password!.text, !pwd.isEmpty else {
            showAlert(message: "You must provide a password")
            return
        }
    directory.user.name = name
    directory.user.password = pwd
        directory.getKey(success:{self.goToMain()},failure:{self.showAlert(message: "Login failed")})
    }
}

In this example the two guard statements attempt to introduce two new variables, name and pwd. If the guard tests succeed we go ahead and use those variables in the statements after the guard statements.

Showing an alert

Often when something goes wrong in an app you will want to let the user know so they can fix the problem. By far the most common way to do this is to display an alert. In the login code above we added some validation code in the guard statements that checks to see if the user entered a name and a password. If they did not, we call a showAlert() method to display the appropriate message to the user.

Here is the code for the showAlert() method:

func showAlert(message msg : String) {
    let alert = UIAlertController(title:nil,message: msg,preferredStyle: .alert)
    let ok = UIAlertAction(title:"OK",style:.default) { action in }
    alert.addAction(ok)
    self.present(alert,animated: true,completion: nil)
}

Alerts come in several different styles. This example shows the most basic style, the .alert style. Alerts typically display a message and give the user one or more options to respond. For each of the response options we add an action to the alert. Actions have a title, a style, and an action closure. These actions will show up as buttons in the alert. Clicking any of the buttons will run any code that you specified in the action closure and then dismiss the alert. In this example the action closure does nothing: all that we want to do in this simple example is to display the alert and dismiss it when the user clicks OK.

You can read more about alerts in the textbook in chapter 14.

Error checking

Once we have validated the user's inputs we are ready to run some sort of computational process. Almost every process includes the possibility that things will go wrong, so we need to include error checks in our logic and be prepared to respond to errors when they happen.

In this example, we have to be prepared for the log in and new user processes to go wrong. The initial validation checks simply check to see that the user has entered a user name and password. Only later in the process can we determine whether or not those inputs are valid.

To implement error checking we have to start by being prepared for things to go wrong. In the example code above the login method calls the Directory's getKey() method to check the user name and password and fetch a key for the user. In preparation of the possiblity that the log in process may fail, I modified the getKey() method to take two parameters. The first is a success callback that will get triggered on a successful login, while the second is a failure callback that getKey() will invoke if the login fails. The success callback calls a method that navigates the app to the directory list view, while the failure callback displays an error message and keeps us in the log in view.

Here is the code for the modified getKey() method, which now contains more careful error checking code:

func getKey(success callback : @escaping () -> Void,failure complain : @escaping () -> Void) {
    if !user.name.isEmpty, !user.password.isEmpty {
        let urlStr = "https://cmsc106.net/directory/users?user=\(user.name)&password=\(user.password)"
        let url = URL(string: urlStr)
        let task = URLSession.shared.dataTask(with: url!) { data, response, error in
            guard error == nil,(response as! HTTPURLResponse).statusCode == 200 else {
                DispatchQueue.main.async {
                        complain()
                    }
                return
            }
            self.user.key = String(decoding: data!, as: UTF8.self)
            DispatchQueue.main.async {
                self.saveUser()
            }
            self.refresh(callback)
        }
        task.resume()
    }
}

The new error checking code appears in the closure we pass to the dataTask. I added a guard statement here that checks both that the error object in the closure is nil and that the response that came back from the server has the correct 200 response code. If anything goes wrong we arrange for the failure closure to run and then give up.