How do I handle background downloads with Alamofire?
Background downloads are essential for iOS applications that need to download large files while the app is in the background or when the device is locked. Alamofire provides robust support for background downloads through URLSessionDownloadTask
and background session configurations. This comprehensive guide covers everything you need to implement reliable background downloads in your iOS applications.
Understanding Background Downloads
Background downloads in iOS use a separate process that continues running even when your app is suspended or terminated. The system manages these downloads and notifies your app when they complete. This is particularly useful for downloading large files, media content, or data synchronization tasks.
Basic Background Download Setup
1. Configure Background Session
First, create a background session configuration:
import Alamofire
import Foundation
class BackgroundDownloadManager {
static let shared = BackgroundDownloadManager()
private var backgroundManager: Session
private init() {
let configuration = URLSessionConfiguration.background(
withIdentifier: "com.yourapp.background-downloads"
)
configuration.isDiscretionary = false
configuration.sessionSendsLaunchEvents = true
backgroundManager = Session(
configuration: configuration,
delegate: SessionDelegate(),
serverTrustManager: nil
)
}
}
2. Implement Download Method
Create a method to initiate background downloads:
func downloadFile(from url: URL, to destination: URL) {
let request = AF.download(url, to: { _, _ in
return (destination, [.removePreviousFile, .createIntermediateDirectories])
})
request.downloadProgress { progress in
DispatchQueue.main.async {
print("Download Progress: \(progress.fractionCompleted)")
}
}
request.response { response in
if response.error == nil, let filePath = response.fileURL?.path {
print("File downloaded to: \(filePath)")
}
}
}
Advanced Background Download Implementation
Complete Background Download Manager
Here's a comprehensive implementation with progress tracking and error handling:
import Alamofire
import Foundation
protocol BackgroundDownloadDelegate: AnyObject {
func downloadDidStart(identifier: String)
func downloadDidProgress(identifier: String, progress: Double)
func downloadDidComplete(identifier: String, location: URL?)
func downloadDidFail(identifier: String, error: Error)
}
class BackgroundDownloadManager: NSObject {
static let shared = BackgroundDownloadManager()
weak var delegate: BackgroundDownloadDelegate?
private var backgroundSession: Session
private var activeDownloads: [String: DownloadRequest] = [:]
override init() {
let configuration = URLSessionConfiguration.background(
withIdentifier: "com.yourapp.background-session"
)
configuration.allowsCellularAccess = true
configuration.isDiscretionary = false
configuration.sessionSendsLaunchEvents = true
backgroundSession = Session(
configuration: configuration,
delegate: SessionDelegate(),
serverTrustManager: nil
)
super.init()
}
func startDownload(url: URL, fileName: String) -> String {
let identifier = UUID().uuidString
let destinationURL = getDocumentsDirectory().appendingPathComponent(fileName)
let request = backgroundSession.download(url, to: { _, _ in
return (destinationURL, [.removePreviousFile, .createIntermediateDirectories])
})
// Track progress
request.downloadProgress(queue: .main) { [weak self] progress in
self?.delegate?.downloadDidProgress(
identifier: identifier,
progress: progress.fractionCompleted
)
}
// Handle completion
request.response(queue: .main) { [weak self] response in
self?.activeDownloads.removeValue(forKey: identifier)
if let error = response.error {
self?.delegate?.downloadDidFail(identifier: identifier, error: error)
} else {
self?.delegate?.downloadDidComplete(
identifier: identifier,
location: response.fileURL
)
}
}
activeDownloads[identifier] = request
delegate?.downloadDidStart(identifier: identifier)
return identifier
}
func cancelDownload(identifier: String) {
activeDownloads[identifier]?.cancel()
activeDownloads.removeValue(forKey: identifier)
}
func pauseDownload(identifier: String) {
activeDownloads[identifier]?.suspend()
}
func resumeDownload(identifier: String) {
activeDownloads[identifier]?.resume()
}
private func getDocumentsDirectory() -> URL {
FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask)[0]
}
}
Handling App Delegate Events
Background downloads require proper handling in your App Delegate:
import UIKit
class AppDelegate: UIResponder, UIApplicationDelegate {
var backgroundCompletionHandler: (() -> Void)?
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
backgroundCompletionHandler = completionHandler
}
func applicationDidEnterBackground(_ application: UIApplication) {
// App entered background - downloads continue
print("App entered background, downloads continuing...")
}
func applicationWillEnterForeground(_ application: UIApplication) {
// App returning to foreground - check download status
print("App entering foreground, checking download status...")
}
}
Custom Session Delegate
For more control over background downloads, implement a custom session delegate:
class BackgroundSessionDelegate: NSObject, URLSessionDownloadDelegate {
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL
) {
print("Download completed: \(location)")
// Handle completed download
}
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64
) {
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
DispatchQueue.main.async {
// Update UI with progress
print("Download progress: \(progress * 100)%")
}
}
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?
) {
if let error = error {
print("Download failed: \(error.localizedDescription)")
}
// Call completion handler if available
DispatchQueue.main.async {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.backgroundCompletionHandler?()
appDelegate.backgroundCompletionHandler = nil
}
}
}
}
Progress Tracking and UI Updates
Implement progress tracking for better user experience:
class DownloadViewController: UIViewController {
@IBOutlet weak var progressView: UIProgressView!
@IBOutlet weak var downloadButton: UIButton!
@IBOutlet weak var statusLabel: UILabel!
private var downloadIdentifier: String?
override func viewDidLoad() {
super.viewDidLoad()
BackgroundDownloadManager.shared.delegate = self
}
@IBAction func startDownload(_ sender: UIButton) {
guard let url = URL(string: "https://example.com/largefile.zip") else { return }
downloadIdentifier = BackgroundDownloadManager.shared.startDownload(
url: url,
fileName: "largefile.zip"
)
downloadButton.isEnabled = false
statusLabel.text = "Starting download..."
}
@IBAction func cancelDownload(_ sender: UIButton) {
if let identifier = downloadIdentifier {
BackgroundDownloadManager.shared.cancelDownload(identifier: identifier)
}
}
}
extension DownloadViewController: BackgroundDownloadDelegate {
func downloadDidStart(identifier: String) {
statusLabel.text = "Download started"
progressView.progress = 0.0
}
func downloadDidProgress(identifier: String, progress: Double) {
progressView.progress = Float(progress)
statusLabel.text = "Downloading... \(Int(progress * 100))%"
}
func downloadDidComplete(identifier: String, location: URL?) {
downloadButton.isEnabled = true
progressView.progress = 1.0
statusLabel.text = "Download completed"
if let location = location {
print("File saved to: \(location)")
}
}
func downloadDidFail(identifier: String, error: Error) {
downloadButton.isEnabled = true
statusLabel.text = "Download failed: \(error.localizedDescription)"
}
}
Error Handling and Retry Logic
Implement robust error handling with automatic retry:
extension BackgroundDownloadManager {
private func handleDownloadError(_ error: Error, for identifier: String, retryCount: Int = 0) {
let maxRetries = 3
if retryCount < maxRetries {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
// Retry download logic here
print("Retrying download (attempt \(retryCount + 1))")
}
} else {
delegate?.downloadDidFail(identifier: identifier, error: error)
}
}
func validateDownloadedFile(at url: URL) -> Bool {
guard FileManager.default.fileExists(atPath: url.path) else {
return false
}
do {
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
let fileSize = attributes[.size] as? Int64 ?? 0
return fileSize > 0
} catch {
return false
}
}
}
Handling Download Resume
Alamofire supports resumable downloads for interrupted transfers:
extension BackgroundDownloadManager {
func resumeDownload(from resumeData: Data, fileName: String) -> String {
let identifier = UUID().uuidString
let destinationURL = getDocumentsDirectory().appendingPathComponent(fileName)
let request = backgroundSession.download(
resumingWith: resumeData,
to: { _, _ in
return (destinationURL, [.removePreviousFile, .createIntermediateDirectories])
}
)
setupDownloadHandlers(for: request, identifier: identifier)
activeDownloads[identifier] = request
return identifier
}
func saveResumeData(for identifier: String, completion: @escaping (Data?) -> Void) {
activeDownloads[identifier]?.cancel { resumeDataOrNil in
completion(resumeDataOrNil)
}
activeDownloads.removeValue(forKey: identifier)
}
private func setupDownloadHandlers(for request: DownloadRequest, identifier: String) {
request.downloadProgress(queue: .main) { [weak self] progress in
self?.delegate?.downloadDidProgress(
identifier: identifier,
progress: progress.fractionCompleted
)
}
request.response(queue: .main) { [weak self] response in
self?.activeDownloads.removeValue(forKey: identifier)
if let error = response.error {
self?.delegate?.downloadDidFail(identifier: identifier, error: error)
} else {
self?.delegate?.downloadDidComplete(
identifier: identifier,
location: response.fileURL
)
}
}
}
}
Managing Multiple Downloads
Handle multiple concurrent downloads efficiently:
extension BackgroundDownloadManager {
func startMultipleDownloads(urls: [URL], fileNames: [String]) -> [String] {
guard urls.count == fileNames.count else {
fatalError("URLs and file names count must match")
}
var identifiers: [String] = []
for (index, url) in urls.enumerated() {
let identifier = startDownload(url: url, fileName: fileNames[index])
identifiers.append(identifier)
}
return identifiers
}
func getActiveDownloadCount() -> Int {
return activeDownloads.count
}
func cancelAllDownloads() {
for (identifier, request) in activeDownloads {
request.cancel()
delegate?.downloadDidFail(
identifier: identifier,
error: URLError(.cancelled)
)
}
activeDownloads.removeAll()
}
func getDownloadProgress(for identifier: String) -> Double? {
return activeDownloads[identifier]?.downloadProgress.fractionCompleted
}
}
Testing Background Downloads
Test your background download implementation:
import XCTest
@testable import YourApp
class BackgroundDownloadTests: XCTestCase {
var downloadManager: BackgroundDownloadManager!
var expectation: XCTestExpectation!
override func setUp() {
super.setUp()
downloadManager = BackgroundDownloadManager.shared
downloadManager.delegate = self
}
func testBackgroundDownload() {
expectation = XCTestExpectation(description: "Background download completes")
let testURL = URL(string: "https://httpbin.org/json")!
let identifier = downloadManager.startDownload(
url: testURL,
fileName: "test.json"
)
wait(for: [expectation], timeout: 30.0)
}
func testDownloadCancellation() {
let testURL = URL(string: "https://httpbin.org/delay/10")!
let identifier = downloadManager.startDownload(
url: testURL,
fileName: "slow.json"
)
// Cancel after a short delay
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.downloadManager.cancelDownload(identifier: identifier)
}
// Verify cancellation
XCTAssertNil(downloadManager.getDownloadProgress(for: identifier))
}
}
extension BackgroundDownloadTests: BackgroundDownloadDelegate {
func downloadDidStart(identifier: String) {
print("Test download started: \(identifier)")
}
func downloadDidProgress(identifier: String, progress: Double) {
print("Test download progress: \(progress)")
}
func downloadDidComplete(identifier: String, location: URL?) {
print("Test download completed: \(String(describing: location))")
expectation.fulfill()
}
func downloadDidFail(identifier: String, error: Error) {
print("Test download failed: \(error)")
XCTFail("Download should not fail: \(error)")
}
}
Best Practices and Considerations
1. Session Configuration
- Use unique identifiers for different download types
- Set appropriate timeout values based on file sizes
- Configure cellular access policies based on user preferences
- Enable discretionary downloads for non-critical content
2. File Management
- Always specify destination URLs with proper file extensions
- Use appropriate file management options (remove previous, create directories)
- Clean up temporary files and failed downloads
- Implement file validation after downloads complete
3. Memory and Performance
- Limit concurrent downloads to avoid overwhelming the system
- Implement proper progress tracking without blocking the UI
- Use appropriate dispatch queues for delegate callbacks
- Monitor memory usage during large file downloads
4. User Experience
- Provide clear progress indicators and status updates
- Allow users to pause, resume, and cancel downloads
- Handle network interruptions gracefully
- Show meaningful error messages for failed downloads
5. Background Execution
- Properly implement App Delegate methods for background handling
- Call completion handlers to inform the system when downloads finish
- Handle app state transitions correctly
- Test thoroughly with app backgrounding and termination
Troubleshooting Common Issues
Downloads Not Resuming After App Termination
Ensure your App Delegate properly handles background session events:
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
// Store completion handler for later use
BackgroundDownloadManager.shared.backgroundCompletionHandler = completionHandler
}
Downloads Failing on Cellular Networks
Configure appropriate cellular access policies:
configuration.allowsCellularAccess = true
configuration.allowsExpensiveNetworkAccess = false
configuration.allowsConstrainedNetworkAccess = false
Memory Issues with Large Files
Use streaming downloads for very large files:
let request = AF.download(url) { _, _ in
return (destinationURL, [.removePreviousFile])
}
// Monitor memory usage
request.downloadProgress { progress in
// Update UI without storing large amounts of data
print("Downloaded: \(progress.completedUnitCount) / \(progress.totalUnitCount) bytes")
}
Conclusion
Background downloads with Alamofire provide a robust solution for downloading content while your app is inactive. By implementing proper session configuration, progress tracking, error handling, and resume capabilities, you can create a seamless download experience that works reliably across different network conditions and app states.
Remember to test thoroughly with various scenarios including app backgrounding, network interruptions, and device restarts. With the comprehensive examples and best practices outlined in this guide, you'll be able to implement reliable background downloads that enhance your app's functionality without compromising user experience.