SSV
+2

Download file từ cơ bản đến nâng cao trong iOS (phần 1)

Introduction

Download file là một kỹ thuật quan trọng trong đối với bất kỳ developer nào, nó giúp ứng dụng của bạn có thể linh hoạt sử dụng các resource như: file, ảnh, video... từ trên internet.

Trong iOS, download file có nhiều cách, nhưng trong bài viết này, chúng ta sẽ cùng nhau tìm hiểu cách đơn giản và hiệu quả nhất. Đó là dùng URL Loading System.

Simple downloads

Khi download file đơn giản, không có yêu cầu gì quá đặc biệt thì chúng ta chỉ cần sử dụng class URLSessionDownloadTask từ URLSession.default là đủ. Đơn giản ở đây có thể hiểu là bạn không quan tâm đến quá trình download, không cần update tiến độ, trạng thái download, hay sử dụng các callback delegate phức tạp...

URLSessionDownloadTask sẽ call một completion handler khi hoàn thành task với kết quả trả về hoặc là download thành công, hoặc là thất bại.

Trong completion handler này, hệ thống có thể trả về một error, chỉ ra lỗi gặp phải phía client, ví dụ: không thể kết nối internet... Nếu như download không có lỗi phía client, task sẽ trả về một URLResponse, chứa thông tin response thành công từ server download.

Trong trường hợp download thành công, ngoài response trả về, chúng ta sẽ nhận được một object kiểu URL thể hiện thông tin url, đường dẫn file vừa download được trong hệ thống. File này sẽ chỉ là file tạm thời, lưu trong thư mục tmp, sẽ bị hệ thống xóa bỏ trong tương lai. Vì vậy, công việc của chúng ta là phải move/copy file tạm thời sang thư mục cần lưu trữ.

Đoạn code dưới đây mô tả việc download một file đơn giản, nếu như kiểm tra không có lỗi thì sẽ copy file vừa download được vào thư mục Documents của app. Để bắt đầu download thì sau khi tạo task, chúng ta chỉ cần gọi resume():

import UIKit

private struct Constants {
    static let simpleDownloadURL = "https://data25.chiasenhac.com/download2/2172/5/2171043-de949f5d/128/Tron%20Tim%20-%20Den_%20MTV%20Band.mp3"
}

class ViewController: UIViewController {

    @IBOutlet private var messageLabel: UILabel!

    @IBAction private func simpleDownloadButtonTapped(_ sender: Any) {
        guard let url = URL(string: Constants.simpleDownloadURL) else {
            return
        }
        let downloadTask = URLSession.shared.downloadTask(with: url) { downloadedURL, response, clientSideError in
            // Handle error nếu cần
            print("Client side error: \(clientSideError?.localizedDescription ?? "nil")")

            // Handle response trả về từ server download nếu cần
            print("Download response code: \((response as? HTTPURLResponse)?.statusCode ?? 0)")

            // Check URL file tạm thời sau khi download thành công
            guard let downloadedURL = downloadedURL else {
                print("Downloaded file URL is nil")
                return
            }
            print("Downloaded temp file: \(downloadedURL.absoluteString)")

            do {
                // Lấy dường dẫn của thư mục document
                let documentURL = try FileManager.default.url(
                    for: .documentDirectory,
                    in: .userDomainMask,
                    appropriateFor: nil,
                    create: false
                )
                // Tạo ra đường dẫn lưu file cuối cùng bằng cách nối thêm đuôi file download
                // vào đường dẫn của thư mục Document
                let saveURL = documentURL.appendingPathComponent(url.lastPathComponent)

                // Xóa file cũ nếu đã tồn tại
                try? FileManager.default.removeItem(at: saveURL)

                // Move file download tạm thời sang đường dẫn mới
                try FileManager.default.moveItem(at: downloadedURL, to: saveURL)
                print("Download file successfully at: \(saveURL.absoluteString)")
                DispatchQueue.main.async {
                    self.messageLabel.text = "Download completed"
                }
            } catch {
                print("File error: \(error.localizedDescription)")
            }
        }
        downloadTask.resume()
    }

}

Khi download thành công, chúng ta sẽ có log như sau:

Client side error: nil
Download response code: 200
Downloaded temp file: file:///Users/nguyen.xuan.thanh/Library/Developer/CoreSimulator/Devices/21C7B80D-576C-4D4E-9B60-19EC33009D79/data/Containers/Data/Application/B34D1DE8-1353-47DA-9982-020C83A14102/tmp/CFNetworkDownload_d9A3E7.tmp
Download file successfully at: file:///Users/nguyen.xuan.thanh/Library/Developer/CoreSimulator/Devices/21C7B80D-576C-4D4E-9B60-19EC33009D79/data/Containers/Data/Application/B34D1DE8-1353-47DA-9982-020C83A14102/Documents/Tron%20Tim%20-%20Den_%20MTV%20Band.mp3

Receive Progress Updates

Nếu muốn update tiến độ của quá trình download, chúng ta phải sử dụng đến delegate của URLSession. Thay vì nhận kết quả của download task bằng completion handler như ở trên thì chúng ta sẽ sử dụng các callback method của protocol URLSessionTaskDelegateURLSessionDownloadDelegate.

Để làm được điều này, bạn cần tự khởi tạo custom instance của URLSession chứ không sử dụng URLSession.default nữa. Đoạn code dưới đây tạo một lazy instance urlSession có default configuration và set luôn delegate của nó thành self:

    private lazy var urlSession = URLSession(
        configuration: .default,
        delegate: self,
        delegateQueue: nil
    )

Tiếp theo, hãy conform protocol URLSessionDelegate và sử dụng urlSession này để tạo download task thay vì URLSession.default như trước:

    @IBAction private func downloadWithProgressButton(_ sender: Any) {
        guard let url = URL(string: Constants.simpleDownloadURL) else {
            return
        }
        let downloadTask = urlSession.downloadTask(with: url)
        downloadTask.resume()
    }

Khi download task bắt đầu, hệ thống sẽ cập nhật tiến độ download thông qua callback method urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:) của protocol URLSessionDownloadDelegate. Chúng ta có thể tính toán tiến độ thông qua số byte đã ghi ra disk trên tổng số byte cần download.

Đoạn code dưới đây implement callback method này, tính tiến độ download và cập nhật lên một UIProgressView. Hãy đảm bảo mọi cập nhật UI đều phải được thực hiện trên main thread.

extension ViewController: URLSessionDownloadDelegate {

    func urlSession(_ session: URLSession,
                    downloadTask: URLSessionDownloadTask,
                    didWriteData bytesWritten: Int64,
                    totalBytesWritten: Int64,
                    totalBytesExpectedToWrite: Int64) {
        // Tính toán tiến độ download dựa trên tổng số byte đã download
        // trên tổng số byte cần download
        let downloadProgress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)

        // Cập nhật tiến độ download lên UIProgressBar trên main thread
        DispatchQueue.main.async {
            self.downloadProgressView.progress = downloadProgress
        }
    }
    
}

Ngoài ra, chúng ta bắt buộc phải implement callback method khi download hoàn tất urlSession(_:downloadTask:didFinishDownloadingTo:).

Trong method này hãy handle tương tự như trong completion handler ở trên. Check response của downloadTask để đảm bảo chắc chắn rằng response từ server là valid. Location của file download thành công tạm thời thì sẽ được lưu ở param location. Cuối cùng, chúng ta vẫn phải move file sang thư mục cần lưu trữ:

    func urlSession(_ session: URLSession,
                    downloadTask: URLSessionDownloadTask,
                    didFinishDownloadingTo location: URL) {
        // Check downloadTask.response nếu cần
        print("Downloaded temp file: \(location.absoluteString)")

        do {
            // Lấy dường dẫn của thư mục document
            let documentURL = try FileManager.default.url(
                for: .documentDirectory,
                in: .userDomainMask,
                appropriateFor: nil,
                create: false
            )

            // Unwrap download url từ downloadTask
            guard let downloadURL = downloadTask.originalRequest?.url else {
                return
            }
            // Tạo ra đường dẫn lưu file cuối cùng bằng cách nối thêm đuôi file download
            // vào đường dẫn của thư mục Document
            let saveURL = documentURL.appendingPathComponent(downloadURL.lastPathComponent)

            // Xóa file cũ nếu đã tồn tại
            try? FileManager.default.removeItem(at: saveURL)

            // Move file download tạm thời sang đường dẫn mới
            try FileManager.default.moveItem(at: location, to: saveURL)
            print("Download file successfully at: \(saveURL.absoluteString)")
            DispatchQueue.main.async {
                self.messageLabel.text = "Download completed"
            }
        } catch {
            print("File error: \(error.localizedDescription)")
        }
    }

Cuối cùng, để handle error, hãy sử dụng method urlSession(_:task:didCompleteWithError:) để handle client-side error nếu cần thiết.

Khi bấm nút download chúng ta sẽ có kết quả sau:

Downloaded temp file: file:///Users/nguyen.xuan.thanh/Library/Developer/CoreSimulator/Devices/21C7B80D-576C-4D4E-9B60-19EC33009D79/data/Containers/Data/Application/BACBF871-4317-4410-832D-356ECFDA373B/tmp/CFNetworkDownload_o35bAK.tmp
Download file successfully at: file:///Users/nguyen.xuan.thanh/Library/Developer/CoreSimulator/Devices/21C7B80D-576C-4D4E-9B60-19EC33009D79/data/Containers/Data/Application/BACBF871-4317-4410-832D-356ECFDA373B/Documents/The%20Playah%20Special%20Performance_%20-%20Soobin.flac

Link project: https://github.com/oNguyenXuanThanh/DownloadTasks


All Rights Reserved