I have this scenario in my app:
I have to execute 2 block of code repeatedly for N times.
in the first block I have to do a network call with Alamofire (I call a HTTP service).
this call returns data and in this data there is a image path that I have to download on device
in the second block I have to download the image and then save it on device
After this start over again for N times.
My problem is that the user can have app in background and I would to like that this process continues when app is in background too.
For this reason, after the Alamofire call, I insert this code for download image:
let config = URLSessionConfiguration.background(withIdentifier: "XXXXXX.XXXX")
let session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
let url = URL(string: "testurl")
let task = session.downloadTask(with: url!)
task.resume()
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
//save image on device and I recall Alamofire for the next call
}
My problem is this: this process doesent execute when app goes in background.
What I'm wrong?
You need to implement the below delegate,
func application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: #escaping () -> Void) {
// send the completion handler as background completion handler
}
Please refer the below answer for more information:
https://stackoverflow.com/a/44140059/1244403
Related
I want my Swift iOS application to fetch the URL it receives via the NSUserActivityTypeBrowsingWeb request.
My email service-provider "wraps" the links (like most of them do), returning a "302 redirect" message. I want to do the initial GET, to get "302 Found" and "Location" header, but I don't need iOS to follow the redirect for me.
I've added the following code:
// Follow the link to trigger click tracking
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else { return }
print(String(data: data, encoding: .utf8)!)
}
task.resume()
This works, but I don't need to follow the redirect and fetch all of the data (as it can be quite a large web page).
Does iOS provide a way of getting the initial HTTP(S) response without following the redirect? (A bit like omitting the --location flag on CURL).
You should implement your custom session with URLSessionTaskDelegate extension and then you can interrupt redirection inside willPerformHTTPRedirection method:
class Redirect : NSObject {
var session: URLSession?
override init() {
super.init()
session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
}
func makeRequest() {
let url = URL(string: "http://gmail.com")!
let task = session?.dataTask(with: url) {(data, response, error) in
guard let data = data else {
return
}
print(String(data: data, encoding: .utf8)!)
}
task?.resume()
}
}
extension Redirect: URLSessionDelegate, URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) {
// Stops the redirection, and returns (internally) the response body.
completionHandler(nil)
}
}
let r = Redirect()
r.makeRequest()
Outputs:
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
here.
</BODY></HTML>
Thanks to iUrii and kleids I got this working, a few points to note:
A callback is needed to get data out of the async task
Updating UILabels can only happen from the main queue (!!) so the DispatchQueue.main.async {} is needed
//
// AppDelegate.swift
// testlinks
//
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// First attempt at handling a universal link
print("Continue User Activity called: ")
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL {
//handle URL
let r = Redirect()
r.makeRequest(url: url, callback: { (location) in
guard let locationURL = location else {return}
print("locationURL", locationURL)
// Show this on our simple example app
DispatchQueue.main.async {
if let vc = self.window?.rootViewController as? ViewController {
vc.result.text = url.absoluteString
vc.originalURL.text = locationURL.absoluteString
}
}
})
}
return true
}
}
// More efficient click-tracking with HTTP GET to obtain the "302" response, but not follow the redirect through to the Location.
// The callback is used to return the Location header back from the async task = thanks #kleids
class Redirect : NSObject {
var session: URLSession?
override init() {
super.init()
session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
}
func makeRequest(url: URL, callback: #escaping (URL?) -> ()) {
let task = self.session?.dataTask(with: url) {(data, response, error) in
guard response != nil else {
return
}
if let response = response as? HTTPURLResponse {
if let l = response.value(forHTTPHeaderField: "Location") {
callback(URL(string: l))
}
}
}
task?.resume()
}
}
extension Redirect: URLSessionDelegate, URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) {
// Stops the redirection, and returns (internally) the response body.
completionHandler(nil)
}
}
I want to make an additional HTTP request after background download/upload in order to confirm that the application finished downloading/uploading. Let me show you a simple example.
First we need to create download/upload task.
let configuration = URLSessionConfiguration.background(withIdentifier: UUID().uuidString)
configuration.sessionSendsLaunchEvents = true
configuration.isDiscretionary = true
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
session.downloadTask(with: largeFileURL).resume()
Then we need to fire some additional request after download/upload finishes. In order to prevent application from being suspended I'm using background task.
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: { [weak self] in
finishBackgroundTask()
})
let task = URLSession.shared.dataTask(with: someURL) { data, response, error in
// Process response.
finishBackgroundTask()
}
task.resume()
}
private func finishBackgroundTask() {
UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
backgroundTaskIdentifier = .invalid
}
The last thing is to implement application delegate method:
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
}
Question
Is it a proper way to make some work after background transfer?
The best approach, if memory serves, is to start the new request before you call the completion block. Be aware, however, that no matter how you do it, if you repeatedly make short requests, the OS will rapidly increase the delay between when a background download finishes and when your app gets relaunched in the background to handle the session events.
I propose to create a completionHandler into your AppDelegate
var backgroundSessionCompletionHandler: (() -> Void)?
Then in the handleEventsForBackgroundURLSession UIApplicationDelegate's method you define you completion handler
func application(_ application: UIApplication, handleEventsForBackgroundURLSession
identifier: String, completionHandler: #escaping () -> Void) {
backgroundSessionCompletionHandler = {
// Execute your additional HTTP request
}
}
The last step is to call this completion handler when the download is finished
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
if let completionHandler = appDelegate.backgroundSessionCompletionHandler {
appDelegate.backgroundSessionCompletionHandler = nil
DispatchQueue.main.async(execute: {
completionHandler()
})
}
}
}
I hope this helps.
I start a download in an action extension (ActionRequestHandler) like this:
private lazy var urlSession: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: "de.stefantrauth.Downloader")
config.sharedContainerIdentifier = "group.de.stefantrauth.Downloader"
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
}()
private func initiateDownloadOfFileFrom(url: URL) {
urlSession.downloadTask(with: url).resume()
completeRequest() // this tells the system the action extension is done with its work
}
Then the download is processed by iOS in the background.
I now want to handle the finished download in my main application AppDelegate, because that is what iOS calls when the download has finished.
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
print("handleEventsForBackgroundURLSession")
urlSessionBackgroundCompletionHandler = completionHandler
}
This method gets called in background after some time as expected.
My AppDelegate also implements URLSessionDelegate and URLSessionDownloadDelegate to process updates for the download.
Especially interesting are
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
print("urlSessionDidFinishEvents")
self.urlSessionBackgroundCompletionHandler?()
self.urlSessionBackgroundCompletionHandler = nil
}
}
and
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("download finished to \(location.absoluteString)")
do {
let documentsURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let savedURL = documentsURL.appendingPathComponent(location.lastPathComponent)
try FileManager.default.moveItem(at: location, to: savedURL)
print("moved file to: \(savedURL.absoluteString)")
} catch {
print ("file error: \(error)")
}
}
Both urlSessionDidFinishEvents and didFinishDownloadingTo are not being called after handleEventsForBackgroundURLSession got called in background. Only after relaunching the app into foreground the delegate methods get called.
Why are they not getting called and what can I do to fix that?
I tried creating the URLSession in handleEventsForBackgroundURLSessionlike this:
private func initUrlSessionWith(identifier: String) {
let config = URLSessionConfiguration.background(withIdentifier: identifier)
config.sharedContainerIdentifier = "group.de.stefantrauth.Downloader"
urlSession = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
}
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
print("handleEventsForBackgroundURLSession")
initUrlSessionWith(identifier: identifier)
urlSessionBackgroundCompletionHandler = completionHandler
}
However this did not fix the problem.
Before you ask: Yes I am testing this on a real device because the simulator has problems with background task handling.
My code is actually correct. Just my way of debugging was the problem. After using console logging instead of debugging via Xcode it was working fine.
See https://forums.developer.apple.com/message/263807#263807 for more details on how to work with background url session debugging.
I would like to process data as it comes in, so I've instiated a URL session like so:
let session = URLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: operationQueue);
I also set the class to be a URLSessionDataDelegate:
class ViewController: UITableViewController, URLSessionDataDelegate{
Lastly, I implement the didReceive data function like this:
func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive data: Data){
print(data)
}
However, the function is never being called.
I run my session like this:
let session1 = session.dataTask(with: url) { (data, response, error) in
print(data!);
}
It prints the data from the callback, but not from the delegate. Any help is greatly appreciated.
EDIT:
I also added the following:
func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: #escaping (URLSession.ResponseDisposition) -> Void){
completionHandler(URLSession.ResponseDisposition.allow);
}
However, the delegate method is still not being called.
The reason this is not working is that you're creating your data task with a completion handler. Instead you must use the dataTask(with:) function instead.
I also missed this in the documentation and spent hours wondering why my delegate methods weren't being called.
From the docs:
By using the completion handler, the task bypasses calls to delegate methods for response and data delivery, and instead provides any resulting
NSData, URLResponse, and NSError objects inside the completion handler. Delegate methods for handling authentication challenges, however, are still called.
I'm following a sample code to download several images and show them in cells. For that, I configured an URLSession like this:
let backgroundConfig = URLSessionConfiguration.background(withIdentifier: "com.myexample.images")
self.backgroundSession = URLSession(configuration: backgroundConfig, delegate: self, delegateQueue: nil)
Then, I perform the download of an image like this:
func downloadImage(imageUrl url: URL, imageId: Int, completion: ImageResult?) -> URLSessionDownloadTask? {
let request = URLRequest(url: url)
let task = backgroundSession.downloadTask(with: request)
// Code here to keep track of the completion handler for this task
task.resume()
return task
}
I also conform to URLSessionDownloadDelegate and implement its didCompleteWithError, didFinishDownloadingTo and urlSessionDidFinishEvents methods. For this last method, I have this implementation:
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate, let completionHandler = appDelegate.backgroundSessionCompletionHandler {
appDelegate.backgroundSessionCompletionHandler = nil
completionHandler()
}
}
And then in AppDelegate:
var backgroundSessionCompletionHandler: (() -> Void)?
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
backgroundSessionCompletionHandler = completionHandler
}
I don't know if this is actually working. I run the app in a simulator, I go to phone's Home to put the app in background state and I'm not able to see if neither urlSessionDidFinishEvents nor app delegate's handleEventsForBackgroundURLSession methods are called. Maybe because the downloads are too fast to see this.
How could I properly test this in a simulator? Am I missing something?
call this method downloadImage on BackgroundFetch of Appdelegate
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Swift.Void)
and then in debug mode, run app on device, then lock your phone and then from XCode - Debug Menu, select Simulate Background Fetch