Building a Download Manager in iOS with Swift — Mobile System Design

⚠️ Disclaimer: This post is for my own learning and revision. It is based on the excellent explanation in this YouTube video. All credit goes to the creator of the video. I am simply documenting the system design process for future reference.

📌 Requirement Gathering

Content type: Only books (text files, not media).
File size: 20 KB — 200 MB.
Threshold: Take input from user.
Simultaneous downloads: ❌ Not supported.
Background downloads: ✅ Supported.
Features: Pause / Resume / Delete.
Resume after app kill: ✅ Supported.
Users: SDK should be reusable and independent.
Metadata: Shown from API (not frequently updated → can use SQL DB).
Encryption: Files are non-encrypted.

📌 Scope

  • Build a framework (SDK) to handle downloads.
  • Only text files (not photos/videos/media).
  • Serial downloads (one at a time).
  • Must support pause, resume, delete.

📌 Technical Requirements

  • Networking → URLSessionDownloadTask.
  • Queueing → Serial DispatchQueue.
  • Database → SQLite to store download info (content ID, URL, progress, local path).
  • Resume downloads → Store state in DB.
  • Offline support → Download continues in background when network is restored.
  • Disk operations → FileManager.

📌 High-Level Design

Components

DownloadManager (Singleton)

  • Exposes public APIs → start, pause, resume, cancel.
  • Registers listeners for UI updates.
  • Delegates work to DownloadService.

DownloadService

  • Handles URLSessionDownloadTask.
  • Communicates progress back to manager.
  • Manages background downloads.
  • Handles listeners (weak references to prevent memory leaks).

StorageService

  • Handles CRUD operations for DB.
  • Manages disk storage (delete, check free space, fetch directory path).
  • ⚠️ Note: This violates Single Responsibility Principle (SRP) since it handles both DB & Disk.

Database Helper

  • Wrapper around SQLite.
  • Stores metadata: id, url, progress, filePath.

📌 Example Low-Level Design: StorageService

import Foundation
import SQLite3
class StorageService {
static let shared = StorageService()
// MARK: - Database Operations
func saveDownload(id: String, url: String, progress: Double, path: String) {
// Insert or update DB record
}
func fetchDownload(id: String) -> DownloadModel? {
// Query SQLite
return nil
}
func deleteDownload(id: String) {
// Delete from DB & disk
}
// MARK: - Disk Operations
func deleteFromDisk(path: String) {
try? FileManager.default.removeItem(atPath: path)
}
func availableDiskSpace() -> Int64 {
if let attrs = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()),
let freeSize = attrs[.systemFreeSize] as? Int64 {
return freeSize
}
return 0
}
func getDirectoryPath() -> String {
return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first ?? ""
}
}

📌 Open Questions

  • Listeners: When VC changes, how does a new VC listen? → Register multiple weak listeners.
  • SRP Violation: Should disk operations move to another service?
  • Scalability: How to extend for parallel downloads in future?→Use a dictionary keyed by contentId mapping to DownloadTask objects, enabling O(1) lookup and management of multiple parallel downloads simultaneously.

✅ Key Takeaways

  • Keep DownloadManager as entry point.
  • DownloadService does networking.
  • StorageService persists data & manages disk.
  • SQLite ensures resume support.
  • Weak listeners prevent retain cycles.

📺 Reference Video: System Design for iOS Download Manager

Hope this article made your Swift journey a bit smoother. Until next time — happy Swifting! 🚀

Learn more Building a Download Manager in iOS with Swift — Mobile System Design

Leave a Reply