Skip to main content

使用Swift实现iOS中的文件下载 – Part 2

FILE DOWNLOADING IN SWIFT – PART 2

基于之前的例子,我们实现了下载,为了方便测试,我们在下载时添加一个覆盖功能。真实生产环境里面,我们需要对远端文件和本地文件做比较以后才决定是否需要下载和覆盖,但开发测试的时候,我们就强行覆盖已存在文件了。

Swift
import Foundation

class FileEngine {
    var URL_RESOURCE_BG = URL(string: "https://example.com/file.zip")!
    
    func fetchResourcePackage(completion: @escaping (URL?, Error?) -> ()) {
        let task = URLSession.shared.downloadTask(with: URL_RESOURCE_BG) { (location: URL?, response: URLResponse?, error: Error?) in
            guard let location = location, error == nil else {
                completion(nil, error)
                return
            }

            do {
                let fileManager = FileManager.default
                let tmpDirectory = fileManager.temporaryDirectory
                let targetURL = tmpDirectory.appendingPathComponent(self.URL_RESOURCE_BG.lastPathComponent)

                // Remove old file if it exists

                if fileManager.fileExists(atPath: targetURL.path) {

                    try fileManager.removeItem(at: targetURL)

                }

                

                // Move new file

                try fileManager.moveItem(at: location, to: targetURL)

                completion(targetURL, nil)

            } catch {
                print("File error: \(error)")
                completion(nil, error)
            }
        }
        task.resume()
    }
}

在do块中,在将新文件移动到目标位置之前,我们检查该位置是否已经存在一个文件。如果存在,我们使用fileManager.removeItem(at:)来移除它。这样有效地用新文件覆盖了旧文件。

下面我们要为监测下载进度做准备。

要跟踪下载任务的进度,你可以使用URLSessionDownloadDelegate协议的方法,这些方法提供了下载进度、完成情况以及发生的任何错误的更新。

你可以将URLSession的代理设置为一个符合URLSessionDownloadDelegate协议的类的对象,并实现urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)方法来获取下载进度的更新。

Swift
import Foundation

class FileEngine: NSObject, URLSessionDownloadDelegate {

    var URL_RESOURCE_BG = URL(string: "https://example.com/file.zip")!
    var downloadCompletion: ((URL?, Error?) -> ())?

    func fetchResourcePackage(completion: @escaping (URL?, Error?) -> ()) {
        let configuration = URLSessionConfiguration.default
        let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
        let task = session.downloadTask(with: URL_RESOURCE_BG)
        
        downloadCompletion = completion
        task.resume()
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        do {
            let fileManager = FileManager.default
            let tmpDirectory = fileManager.temporaryDirectory
            let targetURL = tmpDirectory.appendingPathComponent(URL_RESOURCE_BG.lastPathComponent)

            // Remove old file if it exists
            if fileManager.fileExists(atPath: targetURL.path) {
                try fileManager.removeItem(at: targetURL)
            }
            
            // Move new file
            try fileManager.moveItem(at: location, to: targetURL)
            downloadCompletion?(targetURL, nil)
        } catch {
            print("File error: \(error)")
            downloadCompletion?(nil, error)
        }
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
        print("Download progress: \(progress)")
    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let error = error {
            print("Error: \(error)")
            downloadCompletion?(nil, error)
        }
    }
}

在下载过程中,urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)方法会被定期调用,并打印下载进度。注意,进度是一个0.0到1.0之间的值,表示已下载的文件的比例。

下载的完成处理程序现在存储在downloadCompletion属性中,这样它可以从urlSession(:downloadTask:didFinishDownloadingTo:)urlSession(:task:didCompleteWithError:)代理方法中被调用。

到这里我们调用方法以后我们可以看到console中看到下载进度。

好了接下来我们就在SwiftUI中建立一个Progress Bar组件来呈现下载进度吧。下面是一个简单的绘制ProgressBar的例子:

Swift
import SwiftUI

struct ContentView: View {
    @ObservedObject var fileEngine = FileEngine()

    var body: some View {
        VStack {
            Button(action: {
                self.fileEngine.fetchResourcePackage()
            }) {
                Text("Download")
            }

            ProgressView(value: fileEngine.downloadProgress, total: 1)

        }
    }
}

FileEngine.swift中的方法也需要做一些更新,其中主要是继承ObservableObject,并发布一个@Published的变量downloadProgress。这使得SwiftUI可以监听downloadProgress的变化,并在它改变时更新ProgressView。

Swift
class FileEngine: NSObject, URLSessionDownloadDelegate, ObservableObject {
    var URL_RESOURCE_BG = URL(string: "https://example.com/file.zip")!
    var downloadCompletion: ((URL?, Error?) -> ())?
    @Published var downloadProgress: Double = 0.0


    func fetchResourcePackage(completion: @escaping (URL?, Error?) -> ()) {
        let configuration = URLSessionConfiguration.default
        let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
        let task = session.downloadTask(with: URL_RESOURCE_BG)
        
        downloadCompletion = completion
        task.resume()
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        do {
            let fileManager = FileManager.default
            let tmpDirectory = fileManager.temporaryDirectory
            let targetURL = tmpDirectory.appendingPathComponent(URL_RESOURCE_BG.lastPathComponent)

            // Remove old file if it exists
            if fileManager.fileExists(atPath: targetURL.path) {
                try fileManager.removeItem(at: targetURL)
            }
            
            // Move new file
            try fileManager.moveItem(at: location, to: targetURL)
            downloadCompletion?(targetURL, nil)
        } catch {
            print("File error: \(error)")
            downloadCompletion?(nil, error)
        }
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        DispatchQueue.main.async {

            self.downloadProgress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)

        }

    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let error = error {
            print("Error: \(error)")
            downloadCompletion?(nil, error)
        }
    }
}

更新UI必须在主线程上完成。因此,我们使用DispatchQueue.main.async确保进度更新发生在主线程上。

UI更新如下:

Swift
import SwiftUI

struct ContentView: View {
    
    @ObservedObject var fileEngine = FileEngine()

    
    var body: some View {
        VStack {
            Button("Download", action: {
                self.fileEngine.tempDownload()
            })
            
            ProgressView(value: self.fileEngine.downloadProgress, total: 1)

            {

                Text(String(format: "%.0f", self.fileEngine.downloadProgress * 100) + "%")

            }

        }
        .padding()
    }
}

点击下载,进度条和下载进度都能显示了。

Happy Coding!

Development, iOS, Networking, Swift, Tutorial