Adding a completion handler to my NSURLSession in Swift

1.2k views Asked by At

I have the following function inside a User class:

// Attempts to log in the user, given a password.
// Sets user's loginSuccessful to true upon successful login, false otherwise.
// If failed login, user's loginFailedMessage is also updated.
func attemptLogin(password:String) {
    // create a new NSURL pointing to the PHP login script
    let url = NSURL(string: loginScriptLocation)

    // create a new NSMutableURLRequest of type POST
    let request = NSMutableURLRequest(URL: url!)
    request.HTTPMethod = "POST"

    // add POST string with username and password to request
    let postString = "username=\(self.username)&password=\(password)"
    request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding)

    // send the request and get the JSON results
    let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
        data, response, error in
        // if there is an error with the request, print it
        if error != nil {
            println("error: \(error)")
            return
        }
        // otherwise, go ahead and parse JSON results
        var err:NSError?
        var json = NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers, error: &err) as? NSDictionary
        if let parseJSON = json {
            var resultValue = parseJSON["status"] as? String
            if resultValue! == "success" {
                // login successful
                self.loginSuccessful = true
            }
            else {
                // login not successful
                self.loginSuccessful = false
                self.loginFailedMessage = parseJSON["message"] as! String
            }
        }
    }
    task.resume()
}

The problem is...

This attemptLogin() function is called in my LoginViewController when the IBAction for my login UIButton is triggered. After attemptLogin() is called inside this IBAction function, the statements following it check to see if the user's loginSuccessful variable has been set to true. If true, it is meant to segue to a different View Controller (logging the user in); if false, it is meant to display an error message.

But of course this doesn't work because when I'm checking to see if loginSuccessful has been set to true or not (after I call attemptLogin()), my NSURLSession hasn't gotten to that point yet. I feel like I need to add a "closure" or "completion handler" to my NSURLSession but I really don't understand how to do that even after scouring StackOverflow for examples. I'm a huge beginner with iOS programming and asynchronous/synchronous methods so don't judge me!

How can I modify my existing attemptLogin() function so that I can trigger something to happen when the NSURLSession is complete (so that I can segue to another screen in my View Controller or show an Alert to the user if there was an error)?

This is the code in my LoginViewController:

// Triggered when login button is tapped
// Attempts to log in a user with username and password typed in.
// Successful login -> takes user to Explore screen
// Failed login -> shows alert message with error
@IBAction func loginButtonTap(sender: UIButton) {
    if (usernameTextField.text.isEmpty || passwordTextField.text.isEmpty) {
        // show alert saying missing required field
    }
    else {
        // attempt login
        self.myUser.setUsernameWithoutPushingToDatabase(self.usernameTextField.text)
        self.myUser.attemptLogin(self.passwordTextField.text)
        if (self.myUser.didLoginSuccessfully()) {
            // login successful, segue to Explore screen
            self.performSegueWithIdentifier("loginToExploreSegue", sender: self)
        }
        else {
            // login failed, display alert with error message
            var incorrectPassAlert = UIAlertController(title: "Login Failed", message: self.myUser.loginFailedMessage, preferredStyle: UIAlertControllerStyle.Alert)
            incorrectPassAlert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
            self.presentViewController(incorrectPassAlert, animated: true, completion: nil)
        }
    }
}
2

There are 2 answers

5
aahrens On BEST ANSWER

You're right you'd want a closure to be passed to attemptLogin

Try something like this

func attemptLogin(password:String, completion: (Bool) -> Void)

Then

if resultValue! == "success" {
    // login successful
    completion(true)
}
else {
    completion(false)
}

Finally you have to create the closure in loginButtonTap

self.myUser.attemptLogin(self.passwordTextField.text, completion: (successful: Bool) -> Void) { 
   if successful {
        // login was good
   } else {
       // login was bad
   }
})
0
enes On

Your attempLogin function should take a completion handler like the following:

func attemptLogin(password:String, completionHandler: (success: Bool, message: String) -> ()) { your code }

In the method, you should pass the result data to that completion handler:

if resultValue! == "success" {
      // login successful
      self.loginSuccessful = true
      completionHandler(success: True, message: "Login Successful.")
}
else {
      // login not successful
      self.loginSuccessful = false
      self.loginFailedMessage = parseJSON["message"] as! String
      completionHandler(success: False, message: loginFailedMessage)
}

Finally, you can call attempLogin like this:

self.myUser.attemptLogin(self.passwordTextField.text, {(success: Bool, message: String) -> () in
         if(success) {
        //do something
         }
         else {
        // do something or print message etc.
        }
    })