⚠️ 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 toDownloadTask
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