Percolator
Percolator

Reputation: 523

Setting a timeout for a request method

I'm trying to set up a timeout for a request method that checks username availability. When the user types in a username and presses a button, the checkUsername method is called. My code is not working because the code inside Timeout(5.0){} is never executed and timeout never gets the value false. I know this is not the best way to do it but I wanted to give it a try and wonder if this can be modified in some way or do I need a different approach?

var timeout: Bool = false

func usernameAvailable(username: String) -> String{
    let response: String!
    response = Server.checkUsername(username!)

    Timeout(5.0){
      self.timeout = true
    }

    while(!timeout){
        if(response != nil){
           return response
        }
    }
    return "Timeout"
}

The Timeout.swift class looks like this and is working

class Timeout: NSObject{

private var timer: NSTimer?
private var callback: (Void -> Void)?

init(_ delaySeconds: Double, _ callback: Void -> Void){
    super.init()
    self.callback = callback
    self.timer = NSTimer.scheduledTimerWithTimeInterval(NSTimeInterval(delaySeconds),
        target: self, selector: "invoke", userInfo: nil, repeats: false)
}

func invoke(){
    self.callback?()
    // Discard callback and timer.
    self.callback = nil
    self.timer = nil
}

func cancel(){
    self.timer?.invalidate()
    self.timer = nil
}
}

Upvotes: 3

Views: 2552

Answers (2)

Rob
Rob

Reputation: 437592

The code in the Timeout block will never run because the timer will fire on the on the main thread, but you're blocking the main thread with your while loop.

You have another issue here, that you're calling Server.checkUsername(username!) and returning that result, which would suggest that this must be a synchronous call (which is not good). So, this is also likely blocking the main thread there. It won't even try to start the Timeout logic until checkUsername returns.

There are kludgy fixes for this, but in my opinion, this begs for a very different pattern. One should never write code that has a spinning while loop that is polling some completion status. It is much better to adopt asynchronous patterns with completionHandler closures. But without more information on what checkUsername is doing, it's hard to get more specific.

But, ideally, if your checkUsername is building a NSMutableURLRequest, just specify timeoutInterval for that and then have the NSURLSessionTask completion block check for NSError with domain of NSURLErrorDomain and a code of NSURLError.TimedOut. You also probably want to cancel the prior request if it's already running.

func startRequestForUsername(username: String, timeout: NSTimeInterval, completionHandler: (Bool?, NSError?) -> ()) -> NSURLSessionTask {
    let request = NSMutableURLRequest(URL: ...)  // configure your request however appropriate for your web service
    request.timeoutInterval = timeout            // but make sure to specify timeout

    let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
        dispatch_async(dispatch_get_main_queue()) {
            guard data != nil && error == nil else {
                completionHandler(nil, error)
                return
            }

            let usernameAvailable = ...  // parse the boolean success/failure out of the `data` however appropriate
            completionHandler(usernameAvailable, nil)
        }
    }
    task.resume()

    return task
}

And you can then use it like so:

private weak var previousTask: NSURLSessionTask?

func checkUsername(username: String) {
    // update the UI to say that we're checking the availability of the user name here, e.g. 

    usernameStatus.text = "Checking username availability..."

    // now, cancel prior request (if any)

    previousTask?.cancel()

    // start new request

    let task = startRequestForUsername(username, timeout: 5) { usernameAvailable, error in
        guard usernameAvailable != nil && error == nil else {
            if error?.domain == NSURLErrorDomain && error?.code == NSURLError.TimedOut.rawValue {
                // everything is cool, the task just timed out
            } else if error?.domain == NSURLErrorDomain && error?.code != NSURLError.Cancelled.rawValue {
                // again, everything is cool, the task was cancelled
            } else {
                // some error other  happened, so handle that as you see fit
                // but the key issue that if it was `.TimedOut` or `.Cancelled`, then don't do anything
            }
            return
        }

        if usernameAvailable! {
            // update UI to say that the username is available

            self.usernameStatus.text = "Username is available"
        } else {
            // update UI to say that the username is not available

            self.usernameStatus.text = "Username is NOT available"
        }
    }

    // save reference to this task

    previousTask = task
}

By the way, if you do this sort of graceful, asynchronous processing of requests, you can also increase the timeout interval (e.g. maybe 10 or 15 seconds). We're not freezing the UI, so we can do whatever we want, and not artificially constrain the time allowed for the request.

Upvotes: 0

Daniel Zhang
Daniel Zhang

Reputation: 5858

I see what you are trying to do and it would make more sense to use an existing framework unless you really need/want to write your own networking code.

I would suggest instead to use the timeoutInterval support in an NSURLRequest along with a completion handler on NSURLSession to achieve the solution that you are seeking.

A timeout of the server response can be handled in the completion handler of something like an NSURLSessionDataTask.

Here is a working example to help get you started that retrieves data from the iTunes Store to illustrate how your timeout could be handled:

let timeout = 5 as NSTimeInterval
let searchTerm = "philip+glass"
let url = NSURL(string: "https://itunes.apple.com/search?term=\(searchTerm)")
let request: NSURLRequest = NSURLRequest(URL: url!,
                                         cachePolicy: NSURLRequestCachePolicy.ReloadIgnoringCacheData,
                                         timeoutInterval: timeout)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task: NSURLSessionDataTask = session.dataTaskWithRequest(request, completionHandler: {
    (data, response, error) in
        if response == nil {
            print("Timeout")
        } else {
            print(String(data: data!, encoding: NSUTF8StringEncoding))
        }
    }
)

task.resume()

If you reduce the timeout interval to something short, you can force the timeout to happen.

Upvotes: 1

Related Questions