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.
Related
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
Updating Alamofire to 5.0.4. As the title says taskWillPerformHTTPRedirection is never called.
In Alamofire 4.x we could do something like:
let sessionDelegate = request.session.delegate as! Alamofire.SessionDelegate
sessionDelegate.taskWillPerformHTTPRedirection = { session, task, response, request in
if let url = task.currentRequest?.url {
// look at redirected url & act accordingly
}
}
}
A request's session/delegate has been overhauled in Alamofire 5 and is no longer directly accessible from the request. More specifically, taskWillPerformHTTPRedirection is a closure callback on ClosureEventMonitor. As a sanity check, I tested using some of the other closure callbacks.. and they worked.
// ClosureEventMonitor
let monitor = ClosureEventMonitor()
monitor.requestDidCreateTask = { request, task in
// Event fires
}
let monitor2 = ClosureEventMonitor()
monitor2.taskWillPerformHTTPRedirection = { sess, task, resp, req in
// Event Never fires
}
monitor2.requestDidFinish = { request in
// Event Fires
}
// Set up Session
var session: Session? = Session(startRequestsImmediately: false, eventMonitors: [monitor, monitor2])
let url = URL(string: "https://google.com")!
let urlRequest = URLRequest(url: url)
let trequest = session?.request(urlRequest)
For reference this code is being fired from my AppDelegate func application(_ application: UIApplication, continue userActivity: NSUserActivity for handling deep/universal links.
I'm not exactly sure what I'm missing here. Any help is greatly appreciated. Thank you for your time.
There are three things here:
First, session?.request(urlRequest) will never actually make a request, since you never call resume() (or attach a response handler).
Second, using a one off Session like that is not recommended. As soon as the Session goes out of scope all requests will be cancelled.
Third, EventMonitors cannot interact with the request pipeline, they're only observational. Instead, use Alamofire 5's new RedirectHandler protocol or Redirector type to handle redirects. There is more in our documentation. A simple implementation that customizes the action performed would be:
let redirector = Redirector(behavior: .modify { task, request, response in
// Customize behavior.
})
session?.request(urlRequest).redirect(using: redirector)
In my app I need to send some instructions to server when the user terminated an app. In applicationWillTerminate func I tried to send it, but it never came to server. I tried to use Alamofire and native URLSession but it doesn't work. Does anybody know how can I send it?
I use this code
let request = "\(requestPrefix)setDriverOrderStatus"
if let url = URL(string:request) {
var parameters : [String : String] = [:]
parameters["access_token"] = UserSession.accessToken
parameters["driver_id"] = UserSession.userID
parameters["status"] = status
var req = URLRequest(url: url)
req.httpMethod = HTTPMethod.put.rawValue
do {
req.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
} catch let error {
print(error.localizedDescription)
}
_ = URLSession.shared.dataTask(with: req, completionHandler: { data, response, error in
guard error == nil else {
print(error ?? "error")
return
}
guard let data = data else {
print("Data is empty")
return
}
let json = try! JSONSerialization.jsonObject(with: data, options: [])
print(json)
}).resume
}
One solution that worked for me is to add sleep at the end of the applicationWillTerminate function like this :
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
// Saves changes in the application's managed object context before the application terminates.
// HERE YOU will make you HTTP request asynchronously
self.postLogoutHistory()
// 3 is the number of seconds in which you estimate your request
// will be finished before system terminate the app process
sleep(3)
print("applicationWillTerminate")
// self.saveContext()
}
put breakpoint in applicationWillTerminate and check that, function is getting called or not because applicationWillTerminate is not called everytime when application is getting terminated, especially when user quit application manually from multitasking window, applicationWillTerminate will not get called! When system terminates the application at that time applicationWillTerminate will get called and you will got approximately five seconds to complete your task!! So, it is not good idea to perform network related task on applicationWillTerminate!!
Refer Apple Documentation for applicationWillTerminate, It states,
This method lets your app know that it is about to be terminated and
purged from memory entirely. You should use this method to perform any
final clean-up tasks for your app, such as freeing shared resources,
saving user data, and invalidating timers. Your implementation of this
method has approximately five seconds to perform any tasks and return.
If the method does not return before time expires, the system may kill
the process altogether.
For apps that do not support background execution or are linked
against iOS 3.x or earlier, this method is always called when the user
quits the app. For apps that support background execution, this method
is generally not called when the user quits the app because the app
simply moves to the background in that case. However, this method may
be called in situations where the app is running in the background
(not suspended) and the system needs to terminate it for some reason.
After calling this method, the app also posts a
UIApplicationWillTerminate notification to give interested objects a
chance to respond to the transition.
Problem
I need to execute a synchronous HTTP request, without following redirects, preferably without using instance variables, since this is to be incorporated into the j2objc project.
What have I tried
I have tried using NSURLConnection sendSynchronousRequest, which unfortunately cannot easily be told not to follow redirects.
Background
Before telling me that I should not use synchronous requests, please bear in mind that this code is for emulating Java's HttpUrlConnection, which is inherently synchronous in behavior, for the j2objc project. The implementation of IosHttpUrlConnections' native makeSynchronousRequest currently always follows redirects. It should respect the HttpUrlConnection.instanceFollowRedirects field.
Further research conducted
When using NSUrlConnection in asynchronous mode, a delegate method is called, which allows for enabling/disabling redirects. However, I need synchronous operation.
This answer on NSUrlconnection: How to wait for completion shows how to implement sendSynchronousRequest using an async request. However, I haven't been able to modify it to use a delegate, and thus haven't been able to not follow redirects.
I hope you can help me
You can use a NSURLSession with a semaphore, create like this:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];
NSURLSessionTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (data)
{
// do whatever you want with the data here
}
else
{
NSLog(#"error = %#", error);
}
dispatch_semaphore_signal(semaphore);
}];
[task resume];
// but have the thread wait until the task is done
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
And you have to implement the following method of NSURLSessionTaskDelegate, and call the completionHandler block passing null to stop the redirect.
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
newRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLRequest *))completionHandler
I guess I'll pick up where they left off, but in Swift since it's so many years later.
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let semaphore = DispatchSemaphore(value: 0)
let configuration = URLSessionConfiguration.ephemeral
configuration.timeoutIntervalForRequest = 10
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
// Redirects to google.com
guard let url = URL(string: "https://bit(dot)ly/19BiSHW") else {
return
}
var data: Data?
var response: URLResponse?
var error: Error?
let task = session.dataTask(with: url) { (innerData, innerResponse, innerError) in
// For clarity, we'll call this the data task's completion closure
// Pass the data back to the calling scope
data = innerData
response = innerResponse
error = innerError
semaphore.signal()
}
task.resume()
if semaphore.wait(timeout: .now() + .seconds(15)) == .timedOut {
// The task's completion closure wasn't called within the time out, handle appropriately
} else {
if let e = error as NSError? {
if e.domain == NSURLErrorDomain && e.code == NSURLErrorTimedOut {
print("The request timed out")
}
return
}
if let d = data {
// do whatever you want with the data here, such as print it
// (the data is the HTTP response body)
print(String.init(data: d, encoding: .utf8) ?? "Response data could not be turned into a string")
return
}
if let r = response {
print("No data and no error, but we received a response, we'll inspect the headers")
if let httpResponse = r as? HTTPURLResponse {
print(httpResponse.allHeaderFields)
}
}
}
}
}
extension ViewController: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Swift.Void) {
// Inside the delegate method, we will call the delegate's completion handler
// completionHandler: A block that your handler should call with
// either the value of the request parameter, a modified URL
// request object, or NULL to refuse the redirect and return
// the body of the redirect response.
// I found that calling the block with nil only triggers the
// return of the body of the redirect response if the session is ephemeral
// Calling this will trigger the data task's completion closure
// which signals the semaphore and allows execution to continue
completionHandler(nil)
}
}
What the code is doing:
It is creating an inherently asynchronous task (URLSessionTask), telling it to being execution by calling resume(), then halting the current execution context by waiting on a DispatchSemaphore. This is trick I've seen used, and personally used on many occasions to make something asynchronous behave in a synchronous fashion.
The key point to make is that the code stops execution in the current context. In this example, that context is the main thread (since it is in a UIViewController method), which is generally bad practice. So, if your synchronous code never continues executing (because the semaphore is never signaled) then you UI thread will be stopped forever causing the UI to be frozen.
The final piece is the implementation of the delegate method. The comments suggest that calling completionHandler(nil) should suffice and the documentation supports that. I found that this is only sufficient if you have an ephemeral URLSessionConfiguration. If you have the default configuration, the data task's completion closure doesn't get invoked, so the semaphore never gets signaled, therefore the code to never moves forward. This is what was causing the commenter's/asker's problems of a frozen UI.
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")
}