0

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

enter image description here

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)
        }
AMIT
  • 906
  • 1
  • 8
  • 20
  • See https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background/ – Rob Mar 07 '22 at 05:53
  • FWIW, I notice that you have not accepted answer answers to any of your questions. I’d suggest going through your [historical questions](https://stackoverflow.com/users/8493670/amit?tab=questions) and consider whether you want to accept an answer any answers by clicking on the check mark next to the respective answer. See [What should I do when someone answers my question?](https://stackoverflow.com/help/someone-answers) – Rob May 20 '23 at 17:39

3 Answers3

1

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.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Download progress does not work when the app is in the background. And also resume when it is in foreground. For example, if the app goes background after downloading 10MB, then it resumes from 10MB. In background and suspension mode it not working. https://drive.google.com/file/d/1EY6p4L40Ey54kPHKSU2_9hIWXGOxkPyT/view – AMIT Mar 07 '22 at 14:50
  • It does work. You might not be implementing it correctly (it's not easy) but it does work when you do. – matt Mar 07 '22 at 15:01
  • Everything working fine. Even, when app launch and after reinstating urlsession, resume all task properly. But progress stop only when I manually send to background by double press home button or force quit. Please guide me a right direction. – AMIT Mar 07 '22 at 17:12
  • 1
    Yes, force-quit will stop background sessions. That’s how it works. The user isn’t just saying “leave the app”, but rather “kill this app and stop all background actions associated with it.” – Rob Mar 07 '22 at 18:16
  • @Rob thanks for your response. Let's me know if it it possible to continue downloading process when app goes background by double press home button. If yes please tell me where I am wrong? Please give me an example. – AMIT Mar 08 '22 at 01:25
  • The double tap of the home button is not the problem. All that does is fire up the “app switcher”. The question is what you do when there. If you simply select another app (simply leaving your current app), then the background `URLSession` (outlined in the links, above) will proceed just fine. But if the user swipes/flicks up on an app, then the user is force-quitting it, and background behaviors like background `URLSession` (or background fetch or push notifications) will stop functioning. – Rob Mar 08 '22 at 01:41
  • You are right. And I also observed that if user force-quit, its session (URLSession task) exists. When app becomes foreground it throw error by its delegate. From there I able to resume my previous URLSession. – AMIT Mar 09 '22 at 23:45
  • App force-quitting mode download continuing, please give me an example reference. And I have shared my repository. – AMIT Mar 11 '22 at 00:44
1

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:

  1. 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.

  2. 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()
    
  3. 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).

Rob
  • 415,655
  • 72
  • 787
  • 1,044
1

In quiet/kill mode background process is terminated by OS. So it is not possible.

AMT
  • 33
  • 3
  • This could be posted in question comments or your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center. – Prateek Varshney Oct 30 '22 at 05:50