From my Watch, I send commands to my iOS app. It's not clear why but if the app is in the background I can see some errors:
Error Domain=NSURLErrorDomain Code=-997 "Lost connection to background transfer service"
Can't end BackgroundTask: no background task exists with identifier 383 (0x17f), or it may have already been ended. Break in UIApplicationEndBackgroundTaskError() to debug.
I've already tried to change my configuration to background, have a correct identifier for my config.
Static or Lazy implementation of my SessionManager.
Count for deinit on the process.
Network Session manager
static var sessionManager: SessionManager = {
let configuration = URLSessionConfiguration.background(withIdentifier: UUID().uuidString + ".WatchOS_Background")
configuration.httpShouldSetCookies = false
configuration.httpMaximumConnectionsPerHost = 4
configuration.timeoutIntervalForRequest = 50
configuration.networkServiceType = .background
configuration.isDiscretionary = false
configuration.shouldUseExtendedBackgroundIdleMode = true
if #available(iOS 13.0, *) {
configuration.allowsExpensiveNetworkAccess = true
configuration.allowsConstrainedNetworkAccess = true
}
let sessionManager = Alamofire.SessionManager(configuration: configuration)
sessionManager.delegate.sessionDidBecomeInvalidWithError = { _, error in
if let error = error {
print(error)
}
}
sessionManager.delegate.taskDidComplete = { _, task, error in
if let error = error {
print(error)
}
}
return sessionManager
}()
Request example
func getListFromServer(completion: #escaping (ServiceResponse<[Model1]>) -> Void) {
let header: HTTPHeaders = ["User-Agent": UserAgentHelper.fullUserAgentString]
request("/api/1/XXXX", method: .get, parameters: nil, encoding: nil, headers: header).responseData { [weak self] response in
guard let strongSelf = self else { return }
completion(strongSelf.completionResponse(response))
}
}
Request method
#discardableResult private func request(
_ path: String,
method: HTTPMethod,
parameters: Parameters? = nil,
encoding: ParameterEncoding? = nil,
headers: HTTPHeaders? = nil)
-> DataRequest {
let userEncoding = encoding ?? self.defaultEncoding
let task = beginBackgroundTask()
let dataRequest = NetworkService.sessionManager.request("\(API)\(path)",
method: method,
parameters: parameters,
encoding: userEncoding,
headers: headers)
dataRequest.validate()
self.endBackgroundTask(taskID: task)
return dataRequest
}
Begin and end background task
func beginBackgroundTask() -> UIBackgroundTaskIdentifier {
return UIApplication.shared.beginBackgroundTask(withName: "Background_API", expirationHandler: {})
}
func endBackgroundTask(taskID: UIBackgroundTaskIdentifier) {
UIApplication.shared.endBackgroundTask(taskID)
}
I hope to have a proper implementation from your and a stable request life cycle.
Many thanks for your help and sorry in advance for the lack of technical terms.
Your core problem is that you're not properly handling the expiration of your background tasks. You must end the tasks in their expiration handler explicitly:
let task = UIApplication.shared.beginBackgroundTask(withName: "Background_API") {
UIApplication.shared.endBackgroundTask(task)
}
I suggest you read more here, where an Apple DTS engineer has extensively outlined the requirements and edge cases of the background task handling.
Additionally, Alamofire doesn't really support background sessions. Using a foreground session with background task handling is probably your best bet. Once the Alamofire SessionManager is deinitialized, any requests it has started will be cancelled, even for background sessions.
Finally, calling validate() within an Alamofire response handler is invalid. You should be calling it on the request before the response handler is added, as it's validates the response before handlers are called. If you're calling it afterward it won't be able to pass the error it produces to your response handler.
Related
I am uploading png Images->Base64->jsonData in URLRequest.httpBody size around 6MB. I am using a static NSURLSession with default Configuration and uploading using dataTask on urlsession. Sometimes its successfully uploaded to server sometimes its not and getting below error and nothing is printing at server side. I am not making parallel calls. We are using SSL pinning and handling authentication challenges proper so no SSL authentication error.
iOS device 11.3 and XCode 10 we are using.
Task <58BF437E-7388-4AE4-B676-2485A57CB0CD>.<10> finished with error - code: -999
private lazy var configuration: URLSessionConfiguration = {
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = TimeInterval(120)
configuration.timeoutIntervalForResource = TimeInterval(120)
return configuration
}()
private lazy var urlSession = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil)
func invokeService(methodName : String, collectionName: String, queryDictionary: [String:AnyObject]! = nil, httpMethod: String! = nil) {
// Set up the URL request
let baseUrl: String = WebServiceConstants.hostUrl.qaUrl + "/\(collectionName)" + "/\(methodName)"
// let baseUrl: String = WebServiceConstants.hostUrl.demoUrl + "/\(collectionName)" + "/\(methodName)"
// let baseUrl: String = WebServiceConstants.hostUrl.prod_Url + "/\(collectionName)" + "/\(methodName)"
guard let url = URL(string: baseUrl) else {
return
}
var urlRequest = URLRequest(url: url)
// set up the session
// let configuration = URLSessionConfiguration.default
// configuration.timeoutIntervalForRequest = TimeInterval(120)
// configuration.timeoutIntervalForResource = TimeInterval(120)
//
// let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
urlRequest.httpMethod = httpMethod
do {
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: queryDictionary, options: []) // pass dictionary to nsdata object and set it as request body
}
catch _ {
}
urlRequest.setValue(WebServiceConstants.HTTPStrings.contentTypeJSON, forHTTPHeaderField: WebServiceConstants.HTTPStrings.contentTypeHeader)
// print(queryDictionary)
if AppController.sharedInstance.isAlreadyLogin() && (KeychainWrapper.standard.string(forKey: Constants.UserDefaultKeys.authorizationHeaderValue) != nil) {
let authorizationHeaderValue = WebServiceConstants.HTTPStrings.authorizationHeaderValue + KeychainWrapper.standard.string(forKey: Constants.UserDefaultKeys.authorizationHeaderValue)!
urlRequest.setValue(authorizationHeaderValue, forHTTPHeaderField: WebServiceConstants.HTTPStrings.authorizationHeader)
}
let _ = urlSession.dataTask(with: urlRequest, completionHandler: { [unowned self]
(data, response, error) in
//print(response)
if error != nil {
if error?._code == NSURLErrorTimedOut {
// print(error?.localizedDescription)
let userInfo = [
NSLocalizedDescriptionKey: BWLocalizer.sharedInstance.localizedStringForKey(key:"App_Timeout_Message")
]
let errorTemp = NSError(domain:"", code:-1001, userInfo:userInfo)
self.delegate?.didFailWithError(errorObject: errorTemp)
} else if error?._code == NSURLErrorNotConnectedToInternet {
let userInfo = [
NSLocalizedDescriptionKey: BWLocalizer.sharedInstance.localizedStringForKey(key:"Internet_Not_Available")
]
let errorTemp = NSError(domain:"", code:-1001, userInfo:userInfo)
self.delegate?.didFailWithError(errorObject: errorTemp)
}
else if error?._code == NSURLErrorCancelled {
// canceled
print("Request is cancelled") // Control reaches here on Finished with Error code = -999
self.delegate?.didFailWithError(errorObject: error!)
}
else {
self.delegate?.didFailWithError(errorObject: error!)
}
} else {
do {
if let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]
{
self.delegate?.didReceiveResponse(responseObject: json as AnyObject)
//Implement your logic
print(json)
}
} catch {
self.delegate?.didFailWithError(errorObject: error)
}
}
}).resume()
}
*Added SSL Certificate pinning code *
extension WebserviceHandler : URLSessionDelegate {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let trust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let credential = URLCredential(trust: trust)
let pinner = setupCertificatePinner() // adding CertificateHash
if (!pinner.validateCertificateTrustChain(trust)) {
challenge.sender?.cancel(challenge)
}
if (pinner.validateTrustPublicKeys(trust)) {
completionHandler(.useCredential, credential)
}
else {
completionHandler(.cancelAuthenticationChallenge, nil)
let popUp = UIAlertController(title: "", message: BWLocalizer.sharedInstance.localizedStringForKey(key:"Certificate_Pining_Fail_Message"), preferredStyle: UIAlertController.Style.alert)
popUp.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: {alertAction in popUp.dismiss(animated: true, completion: nil)}))
popUp.addAction(UIAlertAction(title: BWLocalizer.sharedInstance.localizedStringForKey(key:"Action_Go"), style: UIAlertAction.Style.default, handler: { action in
let urlToOpenAppStorepage = URL(string: WebServiceConstants.hostUrl.appstore_url)
let objApp = UIApplication.shared
objApp.openURL(urlToOpenAppStorepage!)
}))
UIApplication.shared.keyWindow?.rootViewController?.present(popUp, animated: true, completion: nil)
}
}
}
func setupCertificatePinner() -> CertificatePinner {
// let baseUrl: String = WebServiceConstants.hostUrl.dev_URL
let baseUrl: String = WebServiceConstants.hostUrl.qaUrl
// let baseUrl: String = WebServiceConstants.hostUrl.demoUrl
// let baseUrl: String = WebServiceConstants.hostUrl.prod_Url
let pinner = CertificatePinner(baseUrl)
/*
You will see something like this:
being challanged! for www.google.co.nz
hash order is usually most specific to least, so the first one is your domain, the last is the root CA
Production
hash: 8U/k3RvTcMVSafJeS9NGpY4KDFdTLwpQ/GUc+lmPH/s=
hash: 4vkhpuZeIkPQ+6k0lXGi7ywkVNV55LhVgU0GaWWMOdk=
hash: jZzMXbxSnIsuAiEBqDZulZ/wCrrpW9bRLMZ6QYxs0Gk=
hash: uthKDtpuYHgn+wRsokVptSysyqBzmr4RP86mOC703bg=
you might need to change the has below to be the second one in the list for the code to pass
QA
hash: LX6ZGwP3Uii+KCZxDxDWlDWijvNI6K/t2906cUzKYM4=
hash: 4vkhpuZeIkPQ+6k0lXGi7ywkVNV55LhVgU0GaWWMOdk=
hash: jZzMXbxSnIsuAiEBqDZulZ/wCrrpW9bRLMZ6QYxs0Gk=
*/
pinner.debugMode = true
pinner.addCertificateHash(WebServiceConstants.HTTPStrings.hashKey)
return pinner
}
Using pinning library : https://github.com/nicwise/certificatepinner
I see several bugs in that code.
The very first line in your authentication handler is going to cause failures if you're behind an authenticated proxy.
It will also fail if the server wants any sort of HTTP auth password or OAuth credential.
It will also fail in a number of other situations.
You should never cancel an authentication request unless the request is actually bad in some way. Canceling the authentication request also prevents the operating system from handling it transparently for you if you can. The only situation where you should cancel an authentication request is if you check a cert or whatever and it is actually invalid. Otherwise, you should generally trust the OS to do the right thing by default when you request external handling.
So use default handling unless the authentication method is server trust.
The code does not check the authentication method at all. You should not be doing any checks unless the authentication method is server trust, for all of the reasons listed above. If it is anything else, use default handling. Always.
The next if statement has two problems:
It provides a new state for the authentication request without returning. This means you can call the completion handler afterwards, which could cause crashes and other misbehavior.
It is calling methods on the challenge sender that are intended to affect behavior. That's how you used to do it with NSURLConnection, but you should never call any methods on the challenge sender (other than possibly to see if it is an object of your own creation, if you are using custom NSURLProtocol classes) with NSURLSession, because it can cause all sorts of problems, up to and including crashes. (See the giant warning in the documentation for NSURLAuthenticationChallenge's -sender method, or, for that matter, the two paragraphs before that warning.)
I'm not entirely sure I trust the pinning code, either. It looks like it passes if any key within the chain of trust is a trusted key, whereas typically pinning requires that the last (leaf) key in the chain of trust be a trusted key.
The security advice in that pinning code is also dubious. You probably shouldn't be pinning to a certificate, but rather to the key inside the certificate. Pinning the leaf cert's key is entirely appropriate, and is really the only appropriate choice, because it is typically the only one whose key is actually under your direct control. If you reissue the cert, that's no big deal, because you should be reissuing with the same key as before, unless your key has been compromised in some way.
Alternatively, you can add a trust layer if you want, but this either requires running your own custom root (which would require changing the validateCertificateTrustChain method to add your custom root cert while validating the chain of trust) or convincing a CA to sell you a cert that can sign other certs, which costs $$$$. Neither of these options seems very practical.
Those issues make me a little bit concerned about the library as a whole, but I don't have time to audit it. You should probably ask around and see if anybody has done a thorough audit of the library in question, as it is notoriously easy to make mistakes when writing code that works with TLS keys, and I'd hate to see you run into security problems later.
After you fix all of the bugs listed above, if you're still having problems, come back and ask another question with updated code. Also, please also ask the code pinning project in question to fix their code snippets, as they seem to contain the same bugs. :-) Thanks.
I am trying to connect to the Twitter streaming API endpoint. It looks like URLSession supports streaming via URLSessionStreamTask, however I can't figure out how to use the API. I have not been able to find any sample code either.
I tried testing the following, but there is no network traffic recorded:
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
let stream = session.streamTask(withHostName: "https://stream.twitter.com/1.1/statuses/sample.json", port: 22)
stream.startSecureConnection()
stream.readData(ofMinLength: 0, maxLength: 100000, timeout: 60, completionHandler: { (data, bool, error) in
print("bool = \(bool)")
print("error = \(String(describing: error))")
})
stream.resume()
I've also implemented the delegate methods (including URLSessionStreamDelegate), but they do not get called.
It would be really helpful if someone code post a sample of how to open a persistent connection for chunked responses from a streaming endpoint. Also, I am seeking solutions which don't involve third party libraries. A response similar to https://stackoverflow.com/a/9473787/5897233 but updated with the URLSession equivalent would be ideal.
Note: Authorization info was omitted from the sample code above.
Received lots of info courtesy of Quinn "The Eskimo" at Apple.
Alas, you have the wrong end of the stick here. URLSessionStreamTask is for wrangling a naked TCP (or TLS over TCP) connection, without the HTTP framing on top. You can think of it as a high-level equivalent to the BSD Sockets API.
The chunked transfer encoding is part of HTTP, and is thus supported by all of the other URLSession task types (data task, upload task, download task). You don’t need to do anything special to enable this. The chunked transfer encoding is a mandatory part of the HTTP 1.1 standard, and is thus is always enabled.
You do, however, have an option as to how you receive the returned data. If you use the URLSession convenience APIs (dataTask(with:completionHandler:) and so on), URLSession will buffer all the incoming data and then pass it to your completion handler in one large Data value. That’s convenient in many situations but it doesn’t work well with a streamed resource. In that case you need to use the URLSession delegate-based APIs (dataTask(with:) and so on), which will call the urlSession(_:dataTask:didReceive:) session delegate method with chunks of data as they arrive.
As for the specific endpoint I was testing, the following was uncovered:
It seems that the server only enables its streaming response (the chunked transfer encoding) if the client sends it a streaming request. That’s kinda weird, and definitely not required by the HTTP spec.
Fortunately, it is possible to force URLSession to send a streaming request:
Create your task with uploadTask(withStreamedRequest:)
Implement the urlSession(_:task:needNewBodyStream:) delegate method to return an input stream that, when read, returns the request body
Profit!
I’ve attached some test code that shows this in action. In this case it uses a bound pair of streams, passing the input stream to the request (per step 2 above) and holding on to the output stream.
If you want to actually send data as part of the request body you can do so by writing to the output stream.
class NetworkManager : NSObject, URLSessionDataDelegate {
static var shared = NetworkManager()
private var session: URLSession! = nil
override init() {
super.init()
let config = URLSessionConfiguration.default
config.requestCachePolicy = .reloadIgnoringLocalCacheData
self.session = URLSession(configuration: config, delegate: self, delegateQueue: .main)
}
private var streamingTask: URLSessionDataTask? = nil
var isStreaming: Bool { return self.streamingTask != nil }
func startStreaming() {
precondition( !self.isStreaming )
let url = URL(string: "ENTER STREAMING URL HERE")!
let request = URLRequest(url: url)
let task = self.session.uploadTask(withStreamedRequest: request)
self.streamingTask = task
task.resume()
}
func stopStreaming() {
guard let task = self.streamingTask else {
return
}
self.streamingTask = nil
task.cancel()
self.closeStream()
}
var outputStream: OutputStream? = nil
private func closeStream() {
if let stream = self.outputStream {
stream.close()
self.outputStream = nil
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: #escaping (InputStream?) -> Void) {
self.closeStream()
var inStream: InputStream? = nil
var outStream: OutputStream? = nil
Stream.getBoundStreams(withBufferSize: 4096, inputStream: &inStream, outputStream: &outStream)
self.outputStream = outStream
completionHandler(inStream)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
NSLog("task data: %#", data as NSData)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error as NSError? {
NSLog("task error: %# / %d", error.domain, error.code)
} else {
NSLog("task complete")
}
}
}
And you can call the networking code from anywhere such as:
class MainViewController : UITableViewController {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if NetworkManager.shared.isStreaming {
NetworkManager.shared.stopStreaming()
} else {
NetworkManager.shared.startStreaming()
}
self.tableView.deselectRow(at: indexPath, animated: true)
}
}
Hope this helps.
So, this is a lot less robust than the example with no explicit task canceling or writing to stream but if you're just YOLO listening to a Server Sent Event stream, this works as of Feb of 2023. It's based on "Use async/await with URLSession" WWDC21 session. That session also has an example for using a custom delegate.
https://developer.apple.com/videos/play/wwdc2021/10095/
func streamReceiverTest(streamURL:URL, session:URLSession) async throws {
let (bytes, response) = try await session.bytes(from:streamURL)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIngError("Not an HTTPResponse")
}
guard httpResponse.statusCode == 200 else {
throw APIngError("Not a success: \(httpResponse.statusCode)")
}
for try await line in bytes.lines {
print(line)
print()
}
}
Inspecting the request with try await streamReceiverTest(streamURL:URL(string:"https://httpbin.org/get")!, session:URLSession.shared) doesn't show that the Accepts header is set, but it seems the API I'm using does need that to offer the stream. Some servers might(?) so I'll include that version as well.
func streamReceiverTestWithManualHeader(streamURL:URL, session:URLSession) async throws {
var request = URLRequest(url:streamURL)
request.setValue("text/event-stream", forHTTPHeaderField:"Accept")
let (bytes, response) = try await session.bytes(for:request)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIngError("Not an HTTPResponse")
}
guard httpResponse.statusCode == 200 else {
throw APIngError("Not a success: \(httpResponse.statusCode)")
}
for try await line in bytes.lines {
print(line)
print()
}
}
I'm developing an iOS download app, that uses Alamofire 4.4 and Swift 3.0.
The app gets a url and downloads the file using Alamofire.
I'm using the background session manager so that the app can download whilst in the background and send a notification when complete.
For some reason, after adding this, the .failure response is never triggered even when putting airplane mode on.
If it turn airplane mode back off, the download weirdly continues going, but leave it long enough and it doesn't continue downloading, so you'd imagine it would have triggered the failure...
I have this code for the download request:
if let resumeData = resumeData {
request = BackendAPIManager.sharedInstance.alamoFireManager.download(resumingWith: resumeData, to: destination)
}
else {
request = BackendAPIManager.sharedInstance.alamoFireManager.download(extUrl, to: destination)
}
alamoRequest = request
.responseData { response in
switch response.result {
case .success:
//get file path of downloaded file
let filePath = response.destinationURL?.path
completeDownload(filePath: filePath!)
case .failure:
//this is what never calls on network drop out
print("Failed")
}
.downloadProgress { progress in
let progressPercent = Float(progress.fractionCompleted)
//handle other progress stuff
}
}
.success case triggers fine, and progress returns fine.
If I cancel a download with alamoRequest?.cancel() then it does trigger the .failure case.
Here's my completion handler:
class BackendAPIManager: NSObject {
static let sharedInstance = BackendAPIManager()
var alamoFireManager : Alamofire.SessionManager!
var backgroundCompletionHandler: (() -> Void)? {
get {
return alamoFireManager?.backgroundCompletionHandler
}
set {
alamoFireManager?.backgroundCompletionHandler = newValue
}
}
fileprivate override init()
{
let configuration = URLSessionConfiguration.background(withIdentifier: "com.uniqudeidexample.background")
self.alamoFireManager = Alamofire.SessionManager(configuration: configuration)
}
}
And in my AppDelegate:
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
BackendAPIManager.sharedInstance.backgroundCompletionHandler = completionHandler
//do other notification stuff...
}
Before I added the background session stuff it worked ok, and when I activated airplane mode it failed. So am I missing something?
Hope that all makes sense, it's my first app, so not clued up on all the technicalities,
Thanks
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
}
}
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.
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.
Recently I read the source code of Alamofire, and I am really confused about How could Alamofire guarantee the response method would be called in correctly order. I hope someone(maybe matt lol) could help me out.
Example, an easy GET Request like this
Alamofire.request(.GET, "https://api.github.com/users/octocat/received_events")
After I analysed the work flow of it, I posted my understanding
Create the request and underlying NSURLSession.
public func request(method: Method, URLString: URLStringConvertible, parameters: [String: AnyObject]? = nil, encoding: ParameterEncoding = .URL) -> Request {
return Manager.sharedInstance.request(method, URLString, parameters: parameters, encoding: encoding)
}
This method would create a request: Request object, which would contain the underlying NSURLSessionDataTask object. Manager.sharedInstance has already set up a NSURLSession and set itself as that session's delegate. The Manager.sharedInstance object would save a customized object request.delegate in its own property delegate
After those objects were created, Alamofire would send this request immediately.
public func request(URLRequest: URLRequestConvertible) -> Request {
var dataTask: NSURLSessionDataTask?
dispatch_sync(queue) {
dataTask = self.session.dataTaskWithRequest(URLRequest.URLRequest)
}
let request = Request(session: session, task: dataTask!)
delegate[request.delegate.task] = request.delegate
if startRequestsImmediately {
request.resume()
}
return request
}
Since Manager.sharedInstance set itself as the underlying NSURLSession's delegate, when data received, the delegate methods would be called
public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
if dataTaskDidReceiveData != nil {
dataTaskDidReceiveData!(session, dataTask, data)
} else if let delegate = self[dataTask] as? Request.DataTaskDelegate {
delegate.URLSession(session, dataTask: dataTask, didReceiveData: data)
}
}
If a user want to get the response and do something related, he would use following public API
// Here the request is a Request object
self.request?.responseString { (request, response, body, error) in
// Something do with the response
}
let's see what the responseString(_: completionHandler:) method does
public func response(queue: dispatch_queue_t? = nil, serializer: Serializer, completionHandler: (NSURLRequest, NSHTTPURLResponse?, AnyObject?, NSError?) -> Void) -> Self {
delegate.queue.addOperationWithBlock {
let (responseObject: AnyObject?, serializationError: NSError?) = serializer(self.request, self.response, self.delegate.data)
dispatch_async(queue ?? dispatch_get_main_queue()) {
completionHandler(self.request, self.response, responseObject, self.delegate.error ?? serializationError)
}
}
return self
}
My question is how 5 could be guaranteed to happen after 3, so the user could get all the response not part of it, because self.response at this time would be fully loaded.
Is it because of NSURLSession's background processing occurs on the same queue -- delegate.queue, which is created like this in Alamofire:
//class Request.TaskDelegate: NSObject, NSURLSessionTaskDelegate
self.queue = {
let operationQueue = NSOperationQueue()
operationQueue.maxConcurrentOperationCount = 1
operationQueue.suspended = true
if operationQueue.respondsToSelector("qualityOfService") {
operationQueue.qualityOfService = NSQualityOfService.Utility
}
return operationQueue
}()
Is my understanding correct, how does that happen? It might be require some understanding about RunLoop and NSURLSession's thread mechanism, if you could point out where I could refer to, thank you as well.