I have an application that communicates with the Apple Watch. When the main iOS app gets a remote notification, it fires off a network request using URLSession and then also sends the result to the watch.
The issue is that when the iOS app is in the background and the push notification is received, the network call is never made. I can see, via breakpoints, that the code is executed but not the code when data is received.
I do have the content-available flag set and I have also tried using the URLSessionConfiguration.background without success.
var config:URLSessionConfiguration!
var session:URLSession?
private func configureSession()
{
self.config = URLSessionConfiguration.background(withIdentifier: "backgroundFetch")
//self.config = URLSessionConfiguration.default
self.config.sessionSendsLaunchEvents = true
self.config.timeoutIntervalForRequest = 15.0
self.config.timeoutIntervalForResource = 15.0
self.session = URLSession(configuration: self.config, delegate: self, delegateQueue: nil)
}
You can see above that I tried it with both default and the background mode.
func getCoordinatesForID(_ trackID:String, withPassCode passCode:String, andMyID myID:String)
{
let url = self.WEB_DOMAIN+"getcoord"
let hash = SecurityModel.sha256(myID+trackID+passCode)!
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded;charset=utf-8", forHTTPHeaderField: "Content-Type")
let postDataStr = "auth=\(hash)&id=\(myID)&trk=\(trackID)&code=\(passCode)"
let requestBodyData = postDataStr.data(using: String.Encoding.utf8)
request.httpBody = requestBodyData
let postDataTask = self.session!.dataTask(with: request)
postDataTask.taskDescription = "getCoordinates"
postDataTask.resume()
}
The above function is called when in the background but this is where things stop. Nothing is ever received. Works fine in foreground.
I do implement the below is neither is called when in the background.
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)
and
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
Update:
Per the comment from Edward I did update capabilities to include "background fetch" although the documentation on downloading after a notification did not indicate this was necessary. That seems to have worked but only in part and so a problem persists.
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
The above is getting called but is throwing the error: "Lost connection to background transfer service". Happens 100% of the time when in the background state but not the foreground.
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)
The above which I need and is called first before the method above it, is not called at all. So overall things have progressed a bit further but still don't work in the same way as it does when in the foreground.
Solution:
The solution was to remove the timeoutIntervalForRequest and timeoutIntervalforResource Despite the fact that I never waited the 15 seconds I set before the issue resulted, removing these allowed the process to work correctly in the background. Additionally I am able to use the standard URLSessionConfiguration.default rather than the .background.
So in the end adding the "background fetch" capability and removing the timeouts resolved the issue.
I found after more tests with the ideas above that things were not working consistently. The issue is that I was using URLSessionDataTask which, as it turns out, is not support in the background. Per Apple's documentation:
Data tasks request a resource, returning the server’s response as one
or more NSData objects in memory. They are supported in default,
ephemeral, and shared sessions, but are not supported in background
sessions.
To solve this problem I had to create a URLSessionDownloadTaskand use its associated delegate
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
With the above the network calls worked properly when the app was in the background.
Related
I've been working on a background file upload to Google Cloud with an iOS app using SwiftUI with Swift 5. I've gotten it to work successfully when everything is configured correctly, but it fails silently and reports success when I intentionally have things incorrectly configured (Incorrect address, bad auth token, etc.). It will even give me data sent updates for the entire upload even though it's not being received.
My question is: How do I get the correct HTTP response headers to tell me that the upload has failed?
Here is the code creating the background url session:
let configuration = URLSessionConfiguration.background(withIdentifier: Constants.URL_SESSION_IDENTIFIER)
configuration.sessionSendsLaunchEvents = true
configuration.sharedContainerIdentifier = "my-suite-name"
self.urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
Here is the code to upload the file:
func uploadFile(fileUrl: URL, token: String, contentType: String, uploadUrl: URL) {
print("Uploading video")
var urlRequest = URLRequest(url: uploadUrl)
urlRequest.setValue(contentType, forHTTPHeaderField: "Content-Type")
urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
urlRequest.httpMethod = "POST"
urlSession.uploadTask(with: urlRequest, fromFile: fileUrl).resume()
}
On the class (self) that is the delegate, I've implemented this protocol URLSessionTaskDelegate and added these two functions:
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { }
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { }
These all work when it's configured correctly, but continue to work even when it's not configured correctly...I can't figure out a way to get them to let me know that the upload isn't actually being received or being rejected. If I try a similar upload with Postman it will immediately respond back with an error and stop running the upload, while this will still run.
I've tried implementing a couple other related protocols with some of their related functions but had no success. For example I found the URLSessionDataDelegate protocol has a function:
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive: URLResponse) { }
Which is the only one in the family of URLSession Delegates that receives http headers that I can find, but it doesn't get triggered when running the uploads.
Thanks for your help and let me know if there is more information needed.
The iOS app I'm working on is using Alamofire 4.9.1 and the following code executes without any issues in iOS 14 and below, but not iOS 15.
dataProvider.sessionDelegate.sessionDidReceiveChallenge = { _, challenge in
print("CHALLENGE ACCEPTED")
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
return (URLSession.AuthChallengeDisposition.useCredential,
cert.urlCredential())
}
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
return (URLSession.AuthChallengeDisposition.useCredential,
URLCredential(trust: challenge.protectionSpace.serverTrust!));
}
return (URLSession.AuthChallengeDisposition.performDefaultHandling,
Optional.none)
}
, where cert is a .pfx certificate initialized just before this.
This is preventing the app from accessing information on a server with TLS 1.2 certificate based authentication. In iOS 13 and 14 (supported iOS versions start at 13) the print statement executes, but not in iOS 15 for some reason. In iOS 13 and 14, in Alamofire's SessionDelegate.swift,
open func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
gets called, but in iOS 15 that is replaced by a call to
open func urlSession(
_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
Any idea as to what can be causing this and how to address it? Thank you in advance.
Interesting that this may have changed, as the documented behavior hasn't: NSURLAuthenticationMethodServerTrust challenges should be received by the session's delegate, not the task's, unless the session delegate doesn't implement the method at all. I would report this issue to Apple so they can either fix the documentation or the delegate callback itself.
In any event, Alamofire 4 is unsupported and this issue will not be fixed. Please update to Alamofire 5, which rectifies this issue by implementing only the URLSessionTaskDelegate version of this method and provides a single hook for this logic.
I managed to solve the issue by modifying the SessionDelegate.swift file of the Alamofire pod itself. Namely, I initialized the certificate in the SessionDelegate.swift file and passed its urlCredential property to the task delegate's credential property in
open func urlSession(
_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
and voilĂ , it worked.
So I re-defined the class (PKCS12) that lets me initialize the certificate inside the SessionDelegate.swift file and the body of the above function now looks like this:
if let taskDidReceiveChallenge = taskDidReceiveChallenge {
let result = taskDidReceiveChallenge(session, task, challenge)
completionHandler(result.0, result.1)
} else if let delegate = self[task]?.delegate {
// this gets executed in iOS 15 and we need to get a credential in order for the delegate to successfully set the disposition required for getting data from the server
let cert = PKCS12.init(mainBundleResource: "\(certNameHere)",
resourceType: "pfx",
password: "%^&^%*&")
delegate.credential = cert.urlCredential()
delegate.urlSession(
session,
task: task,
didReceive: challenge,
completionHandler: completionHandler
)
} else {
urlSession(session, didReceive: challenge, completionHandler: completionHandler)
}
This has solved the issue for us and I hope it is of help to others who may experience similar issues with iOS 15 and Alamofire 4.9.1. Thank you for reading.
I have readed the Alamofire document here.
Something like that:
let rootQueue = DispatchQueue(label: "org.alamofire.customQueue")
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
queue.underlyingQueue = rootQueue
let delegate = SessionDelegate()
let configuration = URLSessionConfiguration.af.default
let urlSession = URLSession(configuration: configuration,
delegate: self,
delegateQueue: queue)
let session = Session(session: urlSession, delegate: delegate, rootQueue: rootQueue)
and you can trust the server certificate throw URLSessionDelegate.
Don't forget set delegate for it.
I'm trying to download using urlsession background session this is my main function
func startfresh() {
session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
let url = URL(string: "https://nava.ir/wp-content/uploads/2018/08/Gholamreza-Sanatgar-Dorooghe-Sefid-128.mp3")
task = session.downloadTask(with: url!)
task.resume()
}
and my didcompletewitherror
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if error != nil {
let err = error as NSError?
let resumeData = err?.userInfo[NSURLSessionDownloadTaskResumeData] as? Data
print("anotherone")
let newtask = session.downloadTask(withResumeData: resumeData!)
newtask.resume()
}
else {
print("hichi")
}
}
but when I close the app when the download is still on progress and relaunch it again and press start download it starts 2 tasks resume previous one and start a new one I want to just resume the previous one with resume data what should I do to just trigger did complete with error method.
What you are seeing is kind of "expected" and you have to design your software to handle it. Actually, there are some more things you should consider. I've investigated and put down at the next as an answer. (NSURLSessionDownloadTask move temporary file)
A sample project is also available.
I'm actually implementing a download functionality in an application.
I'm facing a very weird bug with AVAssetDownloadTask.
Indeed at the beginning of the day the implementation for downloading a asset was working.
When I called the resume function on my AssetDownloadTask, newly created, the download started instantly and the AVAssetDownloadDelegate function
func urlSession(
_ session: URLSession,
assetDownloadTask: AVAssetDownloadTask,
didLoad timeRange: CMTimeRange,
totalTimeRangesLoaded loadedTimeRanges: [NSValue],
timeRangeExpectedToLoad: CMTimeRange
)
was called and showed progress for the active downloading task.
But at one point in the day the process stopped working. Now when I try to start/resume a AssetDownloadTask nothing happens.
No AVAssetDownloadDelegate function is called for the progression of the download. It is like nothing is happening. I don't even get an error.
The weird thing is if I cancelled this AssetDownloadTask the
func urlSession(
_ session: URLSession,
assetDownloadTask: AVAssetDownloadTask,
didFinishDownloadingTo location: URL
)
and
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?
)
are correctly called showing that the process seems to be on hold somehow.
I don't seem to have any of this problems on devices that are running on IOS 11.
I was wondering if any of you had an idea that could explain why my AssetDownloadTask doesn't start/resume on IOS 10.3 devices.
You will find below a few parts of my download manager.
let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
// Create the AVAssetDownloadURLSession using the configuration.
assetDownloadURLSession = AVAssetDownloadURLSession(configuration: backgroundConfiguration, assetDownloadDelegate: self, delegateQueue: OperationQueue.main)
private var activeDownloadsMap = [AVAssetDownloadTask: XXXXXXX]()
func downloadAsset(for asset: XXXXX) {
// Get the default media selections for the asset's media selection groups.
let preferredMediaSelection = asset.geturlAsset().preferredMediaSelection
if #available(iOS 10.0, *) {
guard let task = assetDownloadURLSession.makeAssetDownloadTask(asset: asset.geturlAsset(),
assetTitle: asset.title,
assetArtworkData: nil,
options: nil) else { return }
// To better track the AVAssetDownloadTask we set the taskDescription to something unique for our sample.
task.taskDescription = asset.title
activeDownloadsMap[task] = asset
task.resume()
} else {
return
}
}
extension DownloadManager: AVAssetDownloadDelegate {
public func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange) {
// This delegate callback should be used to provide download progress for your AVAssetDownloadTask.
guard let asset = activeDownloadsMap[assetDownloadTask] else { return }
var percentComplete = 0.0
for value in loadedTimeRanges {
let loadedTimeRange: CMTimeRange = value.timeRangeValue
percentComplete +=
CMTimeGetSeconds(loadedTimeRange.duration) / CMTimeGetSeconds(timeRangeExpectedToLoad.duration)
}
debugPrint("DOWNLOAD: Progress \( assetDownloadTask) \(percentComplete)")
}
}
Thank you in advance for any help you could give me to try and figure this out.
Cant figure out if it is coming from the device / code / OS version
Best regards,
Martin
I have background downloading zip file:
if let url = NSURL(string: urlstring)
{
let config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier((NSUUID().UUIDString))
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.downloadTaskWithURL(url)
session.sessionDescription = filepath
if let sessionId = session.configuration.identifier
{
print("start zip session: " + sessionId)
}
task.resume()
}
}
it works cool if you have internet connection but if you lose it during downloading app just wait and URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) will not be called never.
How it can be handle?
Something like time for response from server
The question is old, but still unanswered, so here is one workaround.
Some clarifications first
In general there is a property of URLSessionConfiguration called waitsForConnectivity which can be set to false where URLSessionTask will then directly fail on connection lost. However if true then URLSessionTaskDelegate will receive a callback on the urlSession(_:taskIsWaitingForConnectivity:) method.
However
Background tasks such as DownloadTask always wait for connectivity and ignore waitsForConnectivity property of URLSessionConfiguration. They also DON'T trigger urlSession(_:taskIsWaitingForConnectivity:) callback, so there is no official way to listen for connectivity drop-out on download task.
Workaround
If you listening for the download progress you'll notice that a call to the method is done few times in a second. Therefore we can conclude that if the progress callback is not called for more than 5 seconds, then there could be a connectivity drop issue. So the workaround is to make additional property to the URLSessionDownloadDelegate delegate and store the last update of the progress. Then have interval function to periodically check whether this property were not updated soon.
Something like:
class Downloader: NSObject, URLSessionTaskDelegate, URLSessionDownloadDelegate {
var lastUpdate: Date;
var downloadTask: URLSessionDownloadTask?;
public var session : URLSession {
get {
let config = URLSessionConfiguration.background(
withIdentifier: "\(Bundle.main.bundleIdentifier!).downloader");
config.isDiscretionary = true;
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue());
}
}
override init() {
self.lastUpdate = Date();
super.init();
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// Handle download completition
// ...
}
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten writ: Int64,
totalBytesExpectedToWrite exp: Int64)
{
let progress = 100 * writ / exp;
// Do something with the progress
// ...
self.lastUpdate = Date();
}
}
var downloader = Downloader();
let url = "http://...";
var request = URLRequest(url: URL(string: url)!);
currentWorker.downloadTask = downloader.session.downloadTask(with: request);
currentWorker.downloadTask!.resume();
// Schedule timer for every 5 secs
var timer = NSTimer.scheduledTimerWithTimeInterval(5.0, target: self, selector: "CheckInternetDrop", userInfo: nil, repeats: true);
func CheckInternetDrop(){
let interval = Date().timeIntervalSinceDate(downloader.lastUpdate);
if (interval > 5) {
print("connection dropped");
}
}
Very simple example...
According to apple docs on pausing and resuming downloads :
Also, downloads that use a background configuration will handle resumption automatically, so manual resuming is only needed for non-background downloads.
Besides, the reason waitsForConnectivity is being ignored is stated in the downloading files in the background :
If your app is in the background, the system may suspend your app while the download is performed in another process.
This is why even if you were to build a timer in a similar fashion to Dimitar Atanasov it will not work once the app goes in the background since it will be suspended.
I've tested the background downloading myself with network failures and the system resumes without fail, hence no extra code is required.
You can set timeouts for your requests:
config.timeoutIntervalForRequest = <desired value>
config.timeoutIntervalForResource = <desired value>
Documentation:
timeoutIntervalForRequest
timeoutIntervalForResource