Table of contents

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:

  1. Use streaming downloads: Alamofire's download methods stream data directly to disk
  2. Avoid loading entire files into memory: Use responseData carefully with large files
  3. Clean up resources: Cancel downloads when views are dismissed
  4. 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

  1. Set appropriate timeout values for large files to prevent premature cancellation
  2. Use background sessions for downloads that may take significant time
  3. Implement retry logic with exponential backoff for failed downloads
  4. Consider chunked downloads for extremely large files
  5. 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.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon