I have several download tasks that I want to run on background. I have a limitation for not downloading more than 3 resources (ex: books and series). Books have just 1 download on it, but series has one download for each chapter (ex: 20 chapters). I made a limit for 2 books and 1 serie (max 10 chapters at a time) for not overloading our server.
Imagine the case I have 10 audios (with 20 chapters each one). I create the tasks for all of it but I just call the resume() method just for the resources that pass the condition of limitation, the rest of them are created but not resumed. The problem is that they never finish more than one resource download (sometimes not even one).
This is my URLSessionConfiguration:
let config = URLSessionConfiguration.background(withIdentifier: bundleIdentifier)
config.isDiscretionary = false
config.sessionSendsLaunchEvents = true
config.httpAdditionalHeaders = ["Authorization": "Bearer " + token]
config.sharedContainerIdentifier = config.identifier
config.waitsForConnectivity = true
config.allowsCellularAccess = true
config.shouldUseExtendedBackgroundIdleMode = true
config.httpMaximumConnectionsPerHost = 12
return config
And my URLSession:
private lazy var session: URLSession = URLSession(configuration: sessionCofig, delegate: self, delegateQueue: OperationQueue())
I have created the callback on my AppDelegate:
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler {
_backgroundCompletionHandler = completionHandler;
}
I also have it implemented on my DownloadManager:
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
guard let appDelegate = AppDelegate.default(), let backgroundCompletionHandler = appDelegate.backgroundCompletionHandler else {
return
}
appDelegate.backgroundCompletionHandler = nil
self.startDownloads()
backgroundCompletionHandler()
}
}
The startDownload method just call task.resume() of the pending downloads (remember the limitation)
Of course I also followed the Apple guide:
https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background
But maybe Im doing something wrong or I misunderstood the guide.
So my questions are:
Should I resume all tasks before going background?
Can it resume tasks on background?
Can handleEventsForBackgroundURLSession be called more than once?
Do you know a way to achieve what Im trying?
Thanks!
Related
After reading several similar questions I don't find anything that fits into my problem.
I have an app that analyzes CSV files (some kind of big data analyzer app). So far CSV files were small and the analysis didn't take too long.
But now we need to support very large CSV files and the analysis takes 5-10 minutes.
If the user closes the app, this process is paused and when the users opens the app again the process is resumed. We need that the analysis continues while the app is in the background.
After reading about different possibilities of running in background I have this:
Use BGTaskScheduler
I'm not sure how to do it. I've followed this official tutorial https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app?language=objc and I've implemented this in my AppDelegate method:
didFinishLaunchingWithOptions {
...
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.mydomain.myapp.myproccess", using: nil) { task in
self.scheduleAppRefresh()
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
...
}
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.mydomain.myapp.myproccess")
request.earliestBeginDate = Date(timeIntervalSinceNow: 1)
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app refresh: \(error)")
}
}
func handleAppRefresh(task: BGAppRefreshTask) {
scheduleAppRefresh()
// Run my task, but how??
}
I don't know how to call the code inside one of my views (in fact one of my presenter cause I'm using VIPER) from appDelegate.
I'm not sure if this is how it works. If I'm not wrong, this BGTaskScheduler is to schedule a task when app goes to background.
Use beginBackgroundTask.
My code was this:
func csvAnalysis() {
DispatchQueue.global(qos: .userInitiated).async {
// Task to analyze CSV
}
}
and now is this:
func csvAnalysis() {
DispatchQueue.global(qos: .userInitiated).async {
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: "mytask") {
UIApplication.shared.endBackgroundTask(self.backgroundTaskID!)
self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid
}
// Task to analyze CSV
UIApplication.shared.endBackgroundTask(self.backgroundTaskID!)
self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid
}
}
But this way, the process keeps pausing after entering background and resuming after opening the app again.
So, is it possible to execute this long task while the app is in background?
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)
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.
The use of invalidateAndCancel() comes up with another problem which is to reinitialize the session again if u need to use it again. After searching on this problem i got the answer of Cnoon's Alamofire Background Service, Global Manager? Global Authorisation Header?. Tried creating the manager but when it is called it never re-initialize the value, to anticipate this problem i did a small cheat i tried giving my session new name each time.
func reset() -> Manager {
let configuration: NSURLSessionConfiguration = {
let identifier = "com.footbits.theartgallery2.background-session\(sessionCount)"
let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(identifier)
return configuration
}()
return Alamofire.Manager(configuration: configuration)
}
func cancelAllRequests() {
manager.session.invalidateAndCancel()
sessionCount += 1
manager = reset()
}
Now i am curious of what is happening in background like the previous session is still there or has been removed by alamofire because if it is still there how many session can i create and what will be the after affect? because i tried:
manager.session.resetWithCompletionHandler {
print("Session reset")
}
it doesnt work.
I know this question is old, but this info will apply to you (the reader) wether or not you are using Alamofire.
You can not revalidate a URLSession, for good reason: The tasks within a session will be told to cancel upon invalidation of the session. However, those tasks may take a while to actually cancel and leave the session. To prove this, cancel a session and retrieve its tasks periodically afterwards. They'll slowly drop towards zero.
What you need to do instead is create a new session with a new identifier. Using the same identifier will cause the old session to be used and crash if new tasks are scheduled to it!
For those seeking to perform background downloads consider using a UUID().uuidString as part of the identifier and storing the id somewhere on the device (Userdefaults come to mind), and generating a new uuid after every call to invalidate your current session. Then create a new session object with a new uuid and reference that instead. Bonus: This new session immediately will have 0 open tasks which proves useful if your logic is based on that.
Example from within my project:
#UserDefaultOptional(defaultValue: nil, "com.com.company.sessionid")
private var sessionID: String?
// MARK: - Init
private var session: URLSession!
private override init() {
super.init()
setupNewSession()
}
private func setupNewSession() {
let id = sessionID ?? UUID().uuidString
sessionID = id
let configuration = URLSessionConfiguration.background(withIdentifier: id)
session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}
private func invalidateSession() {
session.invalidateAndCancel()
sessionID = nil
setupNewSession()
}
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")
}