iOS 中如何实现后台下载

新需求有一个后台下载的功能,由于之前没有涉及过相关内容,于是马上开始搜索 Moya 怎么实现后台下载,一搜才发现,最新的 Moya 已经不支持后台下载了,那就只有用原生的 URLSession 来做了。

先来看一下整个后台下载的流程:

  • 打开应用程序,启动后台下载任务;
  • 进入后台,下载在后台继续;
  • 下载完成后,会调用 UIApplicationDelegateapplication(_ application:handleEventsForBackgroundURLSession:completionHandler:) 方法,此处意思就是如果后台下载完成后,提供一个闭包,可以调用闭包方法告诉系统我们的应用可以被suspend了;
  • 接着会调用 URLSessionDelegateurlSessionDidFinishEvents(forBackgroundURLSession: 方法,在这里我们获取上一步中的闭包进行调用。

好了,接下来看详细的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class BackgroundDownloadManager: NSObject {
    /// **下载进度回调**
    private var progressHandler: ((Double) -> Void)?
   
    /// **下载成功之后的回调**
    private var completionHandler: ((String) -> Void)?

    lazy var session: URLSession = {
        let config = URLSessionConfiguration.background(withIdentifier: "com.demo.download")
        let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
        return session
    }()

    func download(urlString: String, progress: ((Double) -> Void)? = nil, completion: ((String) -> Void)? = nil) {
        self.progressHandler = progress
        self.completionHandler = completion
       
        let url = URL(string: urlString)!
        let urlRequest = URLRequest(url: url)
        let task = session.downloadTask(with: urlRequest)
        task.resume()
    }
}



extension BackgroundDownloadManager: URLSessionDelegate {
    func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        DispatchQueue.main.async {
            if let appDelegate = UIApplication.shared.delegate as? AppDelegate,
                let completionHandler = appDelegate.backgroundCompletionHandler {
                completionHandler()
            }
        }
    }
}



extension BackgroundDownloadManager: URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
        progressHandler?(progress)
    }

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        progressHandler?(1)
        let fileName = dateToString(date: Date(), format: "yyyyMMddHHmmss") + ".tmp"
        let newLocation = NSHomeDirectory() + "/Documents/" + fileName
        do {
            try FileManager.default.moveItem(atPath: location.path, toPath: newLocation)
        } catch {
            print("Move item failed: \(error)")
        }
        completionHandler?(newLocation)
    }

    private func dateToString(date: Date, format: String) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = format
        return dateFormatter.string(from: date)
    }
}

然后在 AppDelegate 实现下面的方法:

1
2
3
4
5
6
7
var backgroundCompletionHandler: (() -> Void)?

...

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        backgroundCompletionHandler = completionHandler
    }

最后开启下载:

1
2
3
4
5
6
7
8
9
10
11
12
let downloadManager = BackgroundDownloadManager()

...

private func startBackgroundDownload() {
    let urlString = "your file url"
    downloadManager.download(urlString: urlString) { progress in
        print("下载进度: \(progress)")
    } completion: { location in
        print("文件已保存到: \(location)")
    }
}

到这里,一个完整的后台下载就结束了。

参考资料
Downloading Files in the Background