How do I handle large file downloads with progress tracking using Alamofire?
Downloading large files efficiently while providing users with progress feedback is a common requirement in iOS applications. Alamofire provides excellent support for file downloads with progress tracking, background downloads, and proper memory management. This comprehensive guide covers everything you need to know about implementing robust file download functionality.
Understanding Alamofire Download Requests
Alamofire's download functionality is built around the download
method, which creates a DownloadRequest
that can handle large files efficiently without loading the entire file into memory. The key advantage is that files are streamed directly to disk rather than being buffered in RAM.
Basic File Download Implementation
Here's a simple example of downloading a file with Alamofire:
import Alamofire
class FileDownloader {
func downloadFile(from url: String, to destination: URL) {
AF.download(url).responseData { response in
switch response.result {
case .success(let data):
do {
try data.write(to: destination)
print("File downloaded successfully")
} catch {
print("Error saving file: \(error)")
}
case .failure(let error):
print("Download failed: \(error)")
}
}
}
}
Implementing Progress Tracking
Progress tracking is essential for large file downloads to provide user feedback. Alamofire makes this straightforward with the downloadProgress
closure:
import Alamofire
import Foundation
class ProgressTrackingDownloader {
private var downloadRequest: DownloadRequest?
func downloadWithProgress(url: String, destination: URL,
progressHandler: @escaping (Double) -> Void,
completion: @escaping (Result<URL, Error>) -> Void) {
downloadRequest = AF.download(url)
.downloadProgress { progress in
let percentComplete = progress.fractionCompleted
DispatchQueue.main.async {
progressHandler(percentComplete)
}
}
.responseData { response in
switch response.result {
case .success(let data):
do {
try data.write(to: destination)
completion(.success(destination))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
func cancelDownload() {
downloadRequest?.cancel()
}
}
Advanced Progress Tracking with Detailed Information
For more detailed progress information, you can access additional properties from the progress object:
func downloadWithDetailedProgress(url: String, destination: URL,
progressHandler: @escaping (Progress) -> Void) {
AF.download(url)
.downloadProgress { progress in
DispatchQueue.main.async {
progressHandler(progress)
}
}
.responseData { response in
// Handle response
}
}
// Usage example
downloader.downloadWithDetailedProgress(url: fileURL, destination: localURL) { progress in
let bytesDownloaded = progress.completedUnitCount
let totalBytes = progress.totalUnitCount
let percentComplete = progress.fractionCompleted
let estimatedTimeRemaining = progress.estimatedTimeRemaining
print("Downloaded: \(bytesDownloaded)/\(totalBytes) bytes (\(percentComplete * 100)%)")
if let timeRemaining = estimatedTimeRemaining {
print("Estimated time remaining: \(timeRemaining) seconds")
}
}
Using Destination Closures for Better File Management
Alamofire provides destination closures for more control over where files are saved and how they're handled:
func downloadToDocuments(url: String, fileName: String,
progressHandler: @escaping (Double) -> Void,
completion: @escaping (Result<URL, Error>) -> Void) {
let destination: DownloadRequest.Destination = { _, _ in
let documentsURL = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask)[0]
let fileURL = documentsURL.appendingPathComponent(fileName)
return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}
AF.download(url, to: destination)
.downloadProgress { progress in
DispatchQueue.main.async {
progressHandler(progress.fractionCompleted)
}
}
.response { response in
if let error = response.error {
completion(.failure(error))
} else if let fileURL = response.fileURL {
completion(.success(fileURL))
}
}
}
Background Downloads for Large Files
For very large files or when you want downloads to continue when the app is in the background, use background sessions:
import Alamofire
class BackgroundDownloadManager {
static let shared = BackgroundDownloadManager()
private let session: Session
private init() {
let configuration = URLSessionConfiguration.background(withIdentifier: "com.yourapp.downloads")
configuration.isDiscretionary = true
configuration.sessionSendsLaunchEvents = true
self.session = Session(configuration: configuration)
}
func downloadInBackground(url: String, fileName: String,
progressHandler: @escaping (Double) -> Void,
completion: @escaping (Result<URL, Error>) -> Void) {
let destination: DownloadRequest.Destination = { _, _ in
let documentsURL = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask)[0]
let fileURL = documentsURL.appendingPathComponent(fileName)
return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}
session.download(url, to: destination)
.downloadProgress { progress in
DispatchQueue.main.async {
progressHandler(progress.fractionCompleted)
}
}
.response { response in
if let error = response.error {
completion(.failure(error))
} else if let fileURL = response.fileURL {
completion(.success(fileURL))
}
}
}
}
Handling Resume Downloads
Alamofire supports resuming interrupted downloads, which is crucial for large files:
class ResumableDownloader {
private var resumeData: Data?
private var downloadRequest: DownloadRequest?
func startOrResumeDownload(url: String, destination: URL,
progressHandler: @escaping (Double) -> Void,
completion: @escaping (Result<URL, Error>) -> Void) {
if let resumeData = self.resumeData {
// Resume existing download
downloadRequest = AF.download(resumingWith: resumeData)
} else {
// Start new download
downloadRequest = AF.download(url)
}
downloadRequest?
.downloadProgress { progress in
DispatchQueue.main.async {
progressHandler(progress.fractionCompleted)
}
}
.responseData { [weak self] response in
switch response.result {
case .success(let data):
do {
try data.write(to: destination)
self?.resumeData = nil // Clear resume data on success
completion(.success(destination))
} catch {
completion(.failure(error))
}
case .failure(let error):
// Store resume data for later use
if let resumeData = response.resumeData {
self?.resumeData = resumeData
}
completion(.failure(error))
}
}
}
func pauseDownload() {
downloadRequest?.cancel { [weak self] resumeData in
self?.resumeData = resumeData
}
}
}
Complete Example with UI Integration
Here's a comprehensive example that integrates with a UIProgressView:
import UIKit
import Alamofire
class DownloadViewController: UIViewController {
@IBOutlet weak var progressView: UIProgressView!
@IBOutlet weak var statusLabel: UILabel!
@IBOutlet weak var downloadButton: UIButton!
private let downloader = ResumableDownloader()
private var isDownloading = false
@IBAction func downloadButtonTapped(_ sender: UIButton) {
if isDownloading {
pauseDownload()
} else {
startDownload()
}
}
private func startDownload() {
let fileURL = "https://example.com/largefile.zip"
let documentsURL = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask)[0]
let destination = documentsURL.appendingPathComponent("largefile.zip")
isDownloading = true
downloadButton.setTitle("Pause", for: .normal)
downloader.startOrResumeDownload(
url: fileURL,
destination: destination,
progressHandler: { [weak self] progress in
self?.updateProgress(progress)
},
completion: { [weak self] result in
self?.handleDownloadCompletion(result)
}
)
}
private func pauseDownload() {
downloader.pauseDownload()
isDownloading = false
downloadButton.setTitle("Resume", for: .normal)
statusLabel.text = "Download paused"
}
private func updateProgress(_ progress: Double) {
progressView.progress = Float(progress)
statusLabel.text = String(format: "Downloading... %.1f%%", progress * 100)
}
private func handleDownloadCompletion(_ result: Result<URL, Error>) {
isDownloading = false
downloadButton.setTitle("Download", for: .normal)
switch result {
case .success(let fileURL):
statusLabel.text = "Download completed!"
print("File saved at: \(fileURL)")
case .failure(let error):
statusLabel.text = "Download failed"
print("Download error: \(error)")
}
}
}
Error Handling and Best Practices
When downloading large files, proper error handling is crucial:
func robustDownload(url: String, destination: URL,
progressHandler: @escaping (Double) -> Void,
completion: @escaping (Result<URL, Error>) -> Void) {
AF.download(url)
.validate(statusCode: 200..<300)
.downloadProgress { progress in
DispatchQueue.main.async {
progressHandler(progress.fractionCompleted)
}
}
.responseData { response in
switch response.result {
case .success(let data):
do {
// Verify file integrity if possible
guard data.count > 0 else {
throw DownloadError.emptyFile
}
try data.write(to: destination)
completion(.success(destination))
} catch {
completion(.failure(error))
}
case .failure(let error):
// Handle specific error types
if let afError = error.asAFError {
switch afError {
case .sessionTaskFailed(let sessionError):
if let urlError = sessionError as? URLError {
switch urlError.code {
case .notConnectedToInternet:
completion(.failure(DownloadError.noInternet))
case .timedOut:
completion(.failure(DownloadError.timeout))
default:
completion(.failure(error))
}
}
default:
completion(.failure(error))
}
} else {
completion(.failure(error))
}
}
}
}
enum DownloadError: Error, LocalizedError {
case emptyFile
case noInternet
case timeout
var errorDescription: String? {
switch self {
case .emptyFile:
return "Downloaded file is empty"
case .noInternet:
return "No internet connection"
case .timeout:
return "Download timed out"
}
}
}
Memory Management Considerations
When downloading large files, it's important to manage memory efficiently:
- Use streaming downloads: Alamofire's download methods stream data directly to disk
- Avoid loading entire files into memory: Use
responseData
carefully with large files - Clean up resources: Cancel downloads when views are dismissed
- Monitor memory usage: Use Instruments to profile memory usage during downloads
class MemoryEfficientDownloader {
private var activeDownloads: [String: DownloadRequest] = [:]
func download(url: String, to destination: URL,
progressHandler: @escaping (Double) -> Void,
completion: @escaping (Result<URL, Error>) -> Void) {
let request = AF.download(url, to: { _, _ in
return (destination, [.removePreviousFile])
})
.downloadProgress { progress in
DispatchQueue.main.async {
progressHandler(progress.fractionCompleted)
}
}
.response { [weak self] response in
self?.activeDownloads.removeValue(forKey: url)
if let error = response.error {
completion(.failure(error))
} else if let fileURL = response.fileURL {
completion(.success(fileURL))
}
}
activeDownloads[url] = request
}
func cancelAllDownloads() {
activeDownloads.values.forEach { $0.cancel() }
activeDownloads.removeAll()
}
deinit {
cancelAllDownloads()
}
}
Console Commands for Testing Downloads
You can test download functionality using various command-line tools:
# Test download speed and behavior with curl
curl -O --progress-bar https://example.com/largefile.zip
# Monitor network activity during downloads
netstat -i 1
# Check available disk space before large downloads
df -h
# Monitor memory usage during app testing
top -pid $(pgrep YourAppName)
Performance Optimization Tips
- Set appropriate timeout values for large files to prevent premature cancellation
- Use background sessions for downloads that may take significant time
- Implement retry logic with exponential backoff for failed downloads
- Consider chunked downloads for extremely large files
- Monitor cellular vs WiFi usage to respect user data preferences
// Configure session for optimal large file downloads
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 60.0
configuration.timeoutIntervalForResource = 300.0
configuration.allowsCellularAccess = false // WiFi only for large downloads
let session = Session(configuration: configuration)
Conclusion
Handling large file downloads with progress tracking in Alamofire requires careful consideration of memory management, user experience, and error handling. The examples provided cover the essential patterns for implementing robust download functionality, including progress tracking, background downloads, resume capability, and proper error handling.
Key takeaways include using Alamofire's streaming download capabilities, implementing proper progress feedback, handling network interruptions gracefully, and managing memory efficiently for large files. Similar principles apply when handling file downloads in Puppeteer or implementing timeout handling in Puppeteer for web scraping scenarios that involve large data transfers.
By following these patterns and best practices, you can create a reliable and user-friendly file download experience in your iOS applications.