iOS 中是否可以下载任何文件应用暂停或安静模式?

发布于 2025-01-12 01:31:43 字数 9274 浏览 1 评论 0原文

我正在寻找一种机制来下载任何文件(视频、音频),不仅用于应用程序背景,还用于应用程序暂停或退出模式。它就像一个android youtube应用程序,后台下载。这意味着操作系统应该处理下载过程。如果有人给我正确的方向,我将不胜感激。

我的项目演示: https://github.com/amitcse6/BackgroundDownloadIOS

下面给出我的实际项目实现。

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)")}
    }
}

函数调用:

        cell.item.setSelectButtonAction(indexPath) { indexPath in
            DownloadManager.shared().startDownload(item.fileDownloadURL, item.originalFileName)
        }

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)
        }

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

淑女气质 2025-01-19 01:31:43

是的,这就是所谓的后台会话。即使您的应用程序已暂停或未运行,系统也会代表您进行下载。请参阅 https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1407496 -background 查看如何创建一个。

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.

掩耳倾听 2025-01-19 01:31:43

如果应用程序被强制退出,后台会话将终止。实际上,强制退出是用户告诉操作系统终止应用程序及其相关后台操作的方式。它会停止所有后台会话、推送通知、后台获取等。

但是,如果用户优雅地离开应用程序,并且即使应用程序随后在其正常生命周期中被抛弃(例如,由于内存压力),那么后台会议将继续进行。


对代码片段的一些不相关的观察:

  1. 不需要场景委托中的handleEventsForBackgroundURLSession。它在应用程序委托上调用。您已在两者中实现了它,但只需要应用程序委托再现。

  2. 可能并不重要,但是共享实现引入了竞争条件并且不是线程安全的。您应该将其简化为以下内容,这是线程安全的:

    static let shared = DownloadManager()
    
  3. 当后台下载完成后重新唤醒您的应用程序时,您如何重新启动 DownloadManager?就我个人而言,我总是让 handleEventsForBackgroundURLSession 将完成处理程序存储在 DownloadManager 的属性中,这样我就知道 DownloadManager 将启动(并将保存的闭包属性保留在逻辑范围内)。

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

牵你手 2025-01-19 01:31:43

在安静/终止模式下,后台进程由操作系统终止。所以这是不可能的。

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文