NSURLSession .waitsForConnectivity flag ignored when DataTask being run on OperationQueue - ios

I have a handwritten class MyURLRequest, that implements Operation. Inside it creates URLSession, configures it
public init(shouldWaitForConnectivity: Bool, timeoutForResource: Double?) {
baseUrl = URL(string: Self.relevantServerUrl + "api/")
self.shouldWaitForConnectivity = shouldWaitForConnectivity
self.timeoutForResource = timeoutForResource
super.init()
localURLSession = URLSession(configuration: localConfig, delegate: self, delegateQueue: nil)
}
public var localConfig: URLSessionConfiguration {
let res = URLSessionConfiguration.default
res.allowsCellularAccess = true
if let shouldWaitForConnectivity = shouldWaitForConnectivity {
res.waitsForConnectivity = shouldWaitForConnectivity
if let timeoutForResource = timeoutForResource {
res.timeoutIntervalForResource = timeoutForResource
}
}
return res
}
creates URLRequest, dataTask, and then being run on OperationQueue. Operation's methods looks like this
override open func start() {
if isCancelled {
isFinished = true
return
}
startDate = Date()
sessionTask?.resume()
localURLSession.finishTasksAndInvalidate()
}
override open func cancel() {
super.cancel()
sessionTask?.cancel()
}
MyURLRequest also implements URLSessionDataDelegate and URLSessionTaskDelegate and the being delegate for it's own URLSession.
There is a problem with waitsForConnectivity NSURLSessionConfiguration's flag. In constructor I set it to true, but this flag is being ignored. In runtime, when network is turned off, request finishes immediately with error -1009. URLSessionTaskDelegate's method urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) is triggered immediately. func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) not being called at all.
The reason definitely not is that flag waitsForConnectivity wasn't correctly set: I've checked config in task received by urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?), and waitsForConnectivity == true.
I also tried to make request without operation queue, and that went fine - behaved such as expected. Maybe have something to do with OperationQueue. Would appreciate your help!
UPDATE:
Seems like root of the problem is that Operation being released too early (when request not complete yet). I've tried to synchronise them using DispatchGroup():
override open func start() {
if isCancelled {
isFinished = true
return
}
startDate = Date()
dispatchGroup.enter()
sessionTask?.resume()
dispatchGroup.wait()
localURLSession.finishTasksAndInvalidate()
}
where .leave() is called in URLSessionDelegate's methods. Nothing changed, still not waiting for connectivity.
UPDATE:
Here's the error I get in didCompleteWithError:
Error Domain=NSURLErrorDomain Code=-1009 "" UserInfo={_kCFStreamErrorCodeKey=50, NSUnderlyingError=0x7fc319112de0 {Error Domain=kCFErrorDomainCFNetwork Code=-1009 "(null)" UserInfo={_kCFStreamErrorCodeKey=50, _kCFStreamErrorDomainKey=1}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <6388AD46-8497-40DF-8768-44FEBB84A8EC>.<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"LocalDataTask <6388AD46-8497-40DF-8768-44FEBB84A8EC>.<1>",
"LocalDataTask <26BCBD73-FC8B-4A48-8EA2-1172ABB8093C>.<1>"
), NSLocalizedDescription=., NSErrorFailingURLStringKey=}

I believe the issue is rooted in your use of finishTasksAndInvalidate. It looks like you are depending on that method synchronously waiting for all pending tasks to complete, but according to the documentation that isn't how it works.
Here's a more in-depth explanation of what I think is happening. By default an Operation is considered complete as soon as the start method returns. This is definitely not what you need, as the task has to be completed asynchronously. Operation is not capable of supporting this behavior out of the box.
Your start returns immediately, long before the session has any time to complete the task you have started. Then, with the operation complete, the queue removes it. This often ends up being the only owner of that instance. If that's true, it kicks off the operation deinit process, which ends up releasing the URLSession. At that point, the session looks like it might do some clean up and terminate any outstanding tasks, forwarding some calls to its delegate. I wasn't sure if URLSession does this, but based on what you are seeing, it sounds like it may.
To achieve what you want, I think you'll need to restructure your NSOperation subclass to be fully asynchronous and to only complete when the started task is done.
Building out a fully thread-safe async NSOperation subclass is a real pain. In case this isn't something you've tackled before, you can check out an implementation here: https://github.com/ChimeHQ/OperationPlus

Related

simple URLSession uploadTask with progress bar

i struggle totally with the URLSession and uploadTask. Actually I just want to upload a json to a Webserver and while the upload is in progress a simple progressbar should be shown.
I implemented the approach given by apple: https://developer.apple.com/documentation/foundation/url_loading_system/uploading_data_to_a_website
the upload is working and i get a response.. so far everything is fine but i don't know how i can show a during the upload a progress bar. What I tried is to show a progressbar before i call the method that contains the upload task
startActivityIndicator()
let jsonPackage = JSONIncident(incident: incident)
jsonPackage.sendToBackend(completion: {
message, error in
//Handle the server response
})
self.activityIndicator.stopAnimating()
UIApplication.shared.endIgnoringInteractionEvents()
I realized that's stupid because the upload task works asynchronious in another thread.
I guess that i have to use Delegates but i don't know in which way. If i implement URLSessionTaskDelegate for example, i have to implement a bunch of functions like isProxy(), isKind(), isMember etc.
Could you please provide me a simple example how to show a progress bar during the uploadtask is working?
That would be so greate!
Thank you very much
regardsChris
You need to conform to the URLSessionTaskDelegate protocol and call this delegate method:
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64)
{
let uploadProgress = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
}
Then use the uploadProgress variable for your progress bar.
Create your session like this:
var session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: OperationQueue.main)
You can use alamofire(for api calling) with swiftyJSON(json parsing) library for uploading JSON. You just have to call a loader before the alamofire api get called and stop the loader when you will get the response.

iOS: Force to cancel Operation in NSOperationQueue

I need to cancel all the operations in the NSOperationQueue immediately.
Consider my scenario,
I have to hit my server continuously ie I will be calling my server whenever user types in the textbox. After user completes his input I have to hit final api call. So I create one NSOperation for single API hit. While user types in the textbox. I create NSOperation object and add that in NSOperationQueue. After detecting that user completely his input, I cancelled all the operation in the queue and hit my final api. The Problem is some operations are not cancelled immediately. So my final api hit is not called immediately. It is waiting for some time (all operation have to finish) and then it called.
FYI,
myOperationQueue.cancelAllOperations()
In Operation start method I have this code
let session = URLSession.shared
let task = session.dataTask(with: urlRequest, completionHandler: { (data, urlResponse, error) -> Void in
})
task.resume()
Please provide me the best way to call my final API immediately.
The apple documentation about the cancellAllOperations method, it clearly explains your situation.
Canceling the operations does not automatically remove them from the queue or stop those that are currently executing. For operations that are queued and waiting execution, the queue must still attempt to execute the operation before recognizing that it is canceled and moving it to the finished state. For operations that are already executing, the operation object itself must check for cancellation and stop what it is doing so that it can move to the finished state. In both cases, a finished (or canceled) operation is still given a chance to execute its completion block before it is removed from the queue.
You have to cancel your task as well.
Implement a cancel override in your Operation subclass. This override cancels the task as well.
var task: URLSessionTask?
func scheduleTask() {
let session = URLSession.shared
///Task is an instance variable
task = session.dataTask(with: urlRequest, completionHandler: { (data, urlResponse, error) -> Void in
})
task?.resume()
}
public override func cancel() {
task?.cancel()
super.cancel()
}

WatchOS 3 WKApplicationRefreshBackgroundTask didReceiveChallenge

I have finally (ignoring the sample code which I never saw work past "application task received, start URL session") managed to get my WatchOS3 code to start a background URL Session task as follows:
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
for task in backgroundTasks {
if let refreshTask = task as? WKApplicationRefreshBackgroundTask {
// this task is completed below, our app will then suspend while the download session runs
print("application task received, start URL session")
let request = self.getRequestForRefresh()
let backgroundConfig = URLSessionConfiguration.background(withIdentifier: NSUUID().uuidString)
backgroundConfig.sessionSendsLaunchEvents = true
backgroundConfig.httpAdditionalHeaders = ["Accept":"application/json"]
let urlSession = URLSession(configuration: backgroundConfig, delegate: self, delegateQueue: nil)
let downloadTask = urlSession.downloadTask(with: request)
print("Dispatching data task at \(self.getTimestamp())")
downloadTask.resume()
self.scheduleNextBackgroundRefresh(refreshDate: self.getNextPreferredRefreshDate())
refreshTask.setTaskCompleted()
}
else if let urlTask = task as? WKURLSessionRefreshBackgroundTask {
//awakened because background url task has completed
let backgroundConfigObject = URLSessionConfiguration.background(withIdentifier: urlTask.sessionIdentifier)
self.backgroundUrlSession = URLSession(configuration: backgroundConfigObject, delegate: self, delegateQueue: nil) //set to nil in task:didCompleteWithError: delegate method
print("Rejoining session ", self.backgroundUrlSession as Any)
self.pendingBackgroundURLTask = urlTask //Saved for .setTaskComplete() in downloadTask:didFinishDownloadingTo location: (or if error non nil in task:didCompleteWithError:)
} else {
//else different task, not handling but must Complete all tasks (snapshot tasks hit this logic)
task.setTaskCompleted()
}
}
}
However, the issue I am now seeing is that my delegate method
urlSession:task:didReceiveChallenge: is never being hit, so I cannot get my download to complete. (I have also added the session level urlSession:didReceiveChallenge: delegate method and it is also not being hit).
Instead I immediately hit my task:didCompleteWithError: delegate method which has the error:
"The certificate for this server is invalid. You might be connecting to a server that is pretending to be ... which could put your confidential information at risk."
Has anyone gotten the background watch update to work with the additional requirement of hitting the didReceiveChallenge method during the background URL session?
Any help or advice you can offer is appreciated.
As it turns out the server certificate error was actually due to a rare scenario in our test environments. After the back end folks gave us a work around for that issue this code worked fine in both our production and test environments.
I never hit urlSession:task:didReceiveChallenge: but it turned out I did not need to.
Made a minor un-related change:
Without prints/breakpoints I was sometimes hitting task:didCompleteWithError Error: like a ms before I hit downloadTask:didFinishDownloadingTo location:.
So I instead set self.pendingBackgroundURLTask completed in downloadTask:didFinishDownloadingTo location:. I only set it completed in task:didCompleteWithError Error: if error != nil.
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
//Complete task only if error, if no error it will be completed when download completes (avoiding race condition)
if error != nil {
self.completePendingBackgroundTask()
}
}
func completePendingBackgroundTask()
{
//Release the session
self.backgroundUrlSession = nil
//Complete the task
self.pendingBackgroundURLTask?.setTaskCompleted()
self.pendingBackgroundURLTask = nil
}
Hope someone else finds this helpful.

Every 11th execution of NSURLSessionTask takes much longer than others

I'm having a strange behavior in my Swift app, that I currently don't understand.
I have subclassed NSOperation to create different operations that can call Rest-WebServices via NSURLSession / NSURLSessionTask. This works fine in general.
In my case I have to execute many of these operations successively. Let's say I create a "chain" of 30 NSOperations with setting dependencies to execute them one by one.
Now I could reproduce the behavior, that every 11th (?!) execution of such an operation, takes much longer than the others. It seems as if the execution "sleeps" for nearly 10 seconds before it goes on. I can rule out, that the concrete web service call is the issue. Because if I change the order of execution, it is still the 11th operation that "hangs".
Currently I am creating a new instance of NSURLSession (defaultConfiguration) during the execution of every operation. Yesterday I tried to create a static instance of NSURLSession and create the instances of NSURLSessionTask during execution only. And now the "hanger" is gone! Unfortunately I could not do it this way, because the NSURLSessionDelegate has to be different for some operations, but this delegate must be passed during initialization.
Did anyone experience a similar behavior?
First I thought my code is too complex to post. But after Ketans comment, I will give it a try. I have trimmed it down to the most important parts. I hope this helps to show my problem. If you need more detail, please let me know.
class AbstractWebServiceOperation: NSOperation {
// VARIANT 2: Create a static NSURLSession only once --> The "sleep" DOES NOT occur!
static let SESSION = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
init(servicePath:String, httpMethod:String) {
// This is an 'abstract' class, that will be subclassed for concrete webService calls that differ in servicePath for URL, HTTP Method and parameters
}
// Override start() function of NSOperation to do webService call. NSOperations vars (ready, executing, finished) are overridden too, to get NSOperation "waiting" for the webService result. But I don't think it is relevant for the issue. So I did leave it out.
override func start() {
super.start()
// [...]
if let request = createRequest()
{
let task = createTask(request)
task.resume()
}
// [...]
}
// Creates the concrete NSURLRequest by using the service path and HTTP method defined by the concrete subclass.
private func createRequest()-> NSMutableURLRequest? {
// [...]
let webServiceURL = "https://\(self.servicePath)"
let url = NSURL(string: webServiceURL)
let request = NSMutableURLRequest(URL: url!)
request.timeoutInterval = 60
request.HTTPMethod = self.httpMethod
request.addValue("application/json;charset=UTF-8", forHTTPHeaderField: "Content-Type")
request.addValue("application/json;charset=UTF-8", forHTTPHeaderField: "Accept")
return request;
}
// Creates the concrete NSURLSessionTask for the given NSURLRequest (using a completionHandler defined by getCompletionHandler())
func createTask(request:NSURLRequest) -> NSURLSessionTask
{
// VARIANT 1: Create a new NSURLSession every time a AbstractWebServiceOperation is executed --> The "sleep" occurs!
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: nil, delegateQueue: nil)
return session.dataTaskWithRequest(request, completionHandler:getCompletionHandler())
// VARIANT 2: Create a static NSURLSession only once --> The "sleep" DOES NOT occur!
return AbstractWebServiceOperation.SESSION.dataTaskWithRequest(request, completionHandler:getCompletionHandler())
}
// Returns the completion handler for the NSURLSessionTask (may be overriden in subclass)
func getCompletionHandler() -> (NSData?, NSURLResponse?, NSError?) -> Void
{
return completionHandler
}
// Default completion handler
lazy var completionHandler:(NSData?, NSURLResponse?, NSError?) -> Void = {(data : NSData?, response : NSURLResponse?, error : NSError?) in
// default completion handling
}
}
Awww... I simply forgot to call session.finishTasksAndInvalidate() to invalidate the session after my webService call is done.
That solves my problem!

iOS: Perform upload task while app is in background

Is there really no way to run an UPLOAD task while an iOS app is in the background? This is ridiculous. Been looking at various stuff like NSURLSessionUploadTask, dispatch_after and even NSTimer, but nothing works for more than the meager 10 seconds the app lives after being put in the background.
How do other apps that have uploads work? Say, uploading an image to Facebook and putting the app in the background, will that cancel the upload?
Why cannot iOS have background services or agents like Android and Windows Phone has?
This is a critical feature of my app, and on the other platforms is works perfectly.
Any help is appreciated :(
You can continue uploads in the background with a “background session”. The basic process of creating a background URLSessionConfiguration with background(withIdentifier:) is outlined in Downloading Files in the Background. That document focuses on downloads, but the same basic process works for upload tasks, too.
Note:
you have to use the delegate-based URLSession;
you cannot use the completion handler renditions of the task factory methods with background sessions;
you also have to use uploadTask(with:fromFile:) method, not the Data rendition ... if you attempt to use uploadTask(with:from:), which uses Data for the payload, with background URLSession you will receive exception with a message that says, “Upload tasks from NSData are not supported in background sessions”; and
your app delegate must implement application(_:handleEventsForBackgroundURLSession:completionHandler:) and capture that completion handler which you can then call in your URLSessionDelegate method urlSessionDidFinishEvents(forBackgroundURLSession:) (or whenever you are done processing the response).
By the way, if you don't want to use background NSURLSession, but you want to continue running a finite-length task for more than a few seconds after the app leaves background, you can request more time with UIApplication method beginBackgroundTask. That will give you a little time (formerly 3 minutes, only 30 seconds in iOS 13 and later) complete any tasks you are working on even if the user leave the app.
See Extending Your App's Background Execution Time. Their code snippet is a bit out of date, but a contemporary rendition might look like:
func initiateBackgroundRequest(with data: Data) {
var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
// Request the task assertion and save the ID.
backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Finish Network Tasks") {
// End the task if time expires.
if backgroundTaskID != .invalid {
UIApplication.shared.endBackgroundTask(backgroundTaskID)
backgroundTaskID = .invalid
}
}
// Send the data asynchronously.
performNetworkRequest(with: data) { result in
// End the task assertion.
if backgroundTaskID != .invalid {
UIApplication.shared.endBackgroundTask(backgroundTaskID)
backgroundTaskID = .invalid
}
}
}
Please don’t get lost in the details here. Focus on the basic pattern:
begin the background task;
supply a timeout clause that cleans up the background task if you happen to run out of time;
initiate whatever you need to continue even if the user leaves the app; and
in the completion handler of the network request, end the background task.
class ViewController: UIViewController, URLSessionTaskDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "http://0.0.0.0")!
let data = "Secret Message".data(using: .utf8)!
let tempDir = FileManager.default.temporaryDirectory
let localURL = tempDir.appendingPathComponent("throwaway")
try? data.write(to: localURL)
let request = URLRequest(url: url)
let config = URLSessionConfiguration.background(withIdentifier: "uniqueId")
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.uploadTask(with: request, fromFile: localURL)
task.resume()
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
print("We're done here")
}

Resources