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.
Related
I'm seeking a mechanism to download any file (video, audio) not only for app background but also for app suspension or quit mode. It is like an android youtube application, background download. That means OS should handle the download process. I'll appreciate it if anybody gives me the right direction.
My project demo: https://github.com/amitcse6/BackgroundDownloadIOS
My actual project implementation is given below.
info.plist
SceneDelegate.swift
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var appCoordinator: AppCoordinator?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let scene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: scene)
self.window = window
GlobalVariable.shared()
self.appCoordinator = AppCoordinator(window: window)
self.appCoordinator?.start()
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
let url = userActivity.webpageURL!
UserActivity.manage(url.absoluteString)
}
}
}
AppDelegate.swift
import UIKit
import IQKeyboardManagerSwift
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
var backgroundCompletionHandler: (() -> Void)?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
IQKeyboardManager.shared.enable = true
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
backgroundCompletionHandler = completionHandler
}
}
DownloadManager.swift
import Foundation
import Photos
import PhotosUI
class DownloadManager: NSObject, ObservableObject {
private static var downloadManager: DownloadManager!
private var urlSession: URLSession!
private var tasks: [URLSessionTask] = []
#discardableResult
public static func shared() -> DownloadManager {
if downloadManager == nil {
downloadManager = DownloadManager()
}
return downloadManager
}
private override init() {
super.init()
//let config = URLSessionConfiguration.default
let config = URLSessionConfiguration.background(withIdentifier: "MySession")
config.isDiscretionary = true
config.sessionSendsLaunchEvents = true
urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil) //OperationQueue.main
updateTasks()
}
func startDownload(_ url: URL) {
let task = urlSession.downloadTask(with: url)
task.resume()
tasks.append(task)
}
func startDownload(_ fileUrl: String?, _ fileName: String?) {
if let fileUrl = fileUrl, let url = URL(string: fileUrl) {
startDownload(url, fileName)
}
}
func startDownload(_ url: URL, _ fileName: String?) {
let task = urlSession.downloadTask(with: url)
task.earliestBeginDate = Date().addingTimeInterval(1)
task.countOfBytesClientExpectsToSend = 200
task.countOfBytesClientExpectsToReceive = 500 * 1024
task.resume()
tasks.append(task)
}
private func updateTasks() {
urlSession.getAllTasks { tasks in
DispatchQueue.main.async {
self.tasks = tasks
}
}
}
}
extension DownloadManager: URLSessionDelegate, URLSessionDownloadDelegate {
func urlSession(_: URLSession, downloadTask: URLSessionDownloadTask, didWriteData _didWriteData: Int64, totalBytesWritten _totalBytesWritten: Int64, totalBytesExpectedToWrite _totalBytesExpectedToWrite: Int64) {
print("Progress \(downloadTask.progress.fractionCompleted) for \(downloadTask) \(_totalBytesWritten) \(_totalBytesExpectedToWrite)")
}
func urlSession(_: URLSession, downloadTask task: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("Download finished: \(location.absoluteString)")
guard
let httpURLResponse = task.response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = task.response?.mimeType else {
print ("Response error!");
return
}
DownloadManager.save((task.currentRequest?.url!)!, location, mimeType, nil)
}
func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print("Download error: \(String(describing: error))")
} else {
print("Task finished: \(task)")
}
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let backgroundCompletionHandler = appDelegate.backgroundCompletionHandler else { return }
backgroundCompletionHandler()
}
}
}
extension DownloadManager {
private static func save(_ url: URL, _ location: URL, _ mimeType: String, _ fileName: String?) {
do {
if mimeType.hasPrefix("image") {
guard let inputImage = UIImage(named: location.path) else { return }
UIImageWriteToSavedPhotosAlbum(inputImage, nil, nil, nil)
}else {
let documentsURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let savedURL = documentsURL.appendingPathComponent((fileName == nil) ? url.lastPathComponent : fileName!)
if FileManager.default.fileExists(atPath: savedURL.path) { try! FileManager.default.removeItem(at: savedURL) }
try FileManager.default.moveItem(at: location, to: savedURL)
DispatchQueue.main.async {
AlertManager.toast("\((fileName == nil) ? url.lastPathComponent : fileName!) download successfully")
}
}
} catch {print ("file error: \(error)")}
}
}
function call:
cell.item.setSelectButtonAction(indexPath) { indexPath in
DownloadManager.shared().startDownload(item.fileDownloadURL, item.originalFileName)
}
Yes, that's called a background session. The system does the download on your behalf, even if your app is suspended or not running. See https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1407496-background to see how to create one.
Background sessions are terminated if the app being force-quit. Force-quitting is, effectively, the user’s way of telling the OS to kill the app and its associated background operations. It stops all background sessions, push notifications, background fetch, etc.
However if the user gracefully leaves the app, and even if the app is subsequently jettisoned in the course of its normal life span (e.g., due to memory pressure), then the background sessions will proceed.
A few unrelated observations on the code snippet:
The handleEventsForBackgroundURLSession in the scene delegate is not needed. It is called on the app delegate. You have implemented it in both, but only the app delegate rendition is needed.
Probably not critical, but the shared implementation introduces a race condition and is not thread-safe. You should simplify that to the following, which is thread-safe:
static let shared = DownloadManager()
When your app is reawakened when the background downloads finish, how are you restarting your DownloadManager? Personally, I have always have handleEventsForBackgroundURLSession store the completion handler in a property of the DownloadManager, that way I know the DownloadManager will be started up (and it keeps this saved closure property in a logical scope).
In quiet/kill mode background process is terminated by OS. So it is not possible.
In my application, I need to download a file(size:50mb), also I need to support background downloading. By the following code I can do the download even in the background. The only problem is UI is not updating properly when I tried to switch the app (say I open WhatsApp) and come back to my app without going to iPhone's Home Screen.
I debugged the code, I found didWriteData is not called.
weak var downloadTask: URLSessionDownloadTask?
lazy var downloadSession: URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier: "MYDOWNLOADIDENTIFIER")
return URLSession(configuration: configuration,
delegate: self,
delegateQueue: OperationQueue.main)
}()
Download Pressed Event:
downloadTask = self.downloadSession.downloadTask(with: fileUrl)
downloadTask?.resume()
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
//UI update for progress level
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
logger.write("urlSessionDidFinishEvents\n")
DispatchQueue.main.async {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let completionHandler = appDelegate.backgroundSessionCompletionHandler {
appDelegate.backgroundSessionCompletionHandler = nil
completionHandler()
}
}
}
AppDelegate.swift
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
backgroundSessionCompletionHandler = completionHandler
}
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
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'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