Designing a high-level architecture for a task manager that handles downloading, deleting, uploading, and canceling tasks for multiple videos simultaneously on iOS involves several key components. This includes defining the system architecture, identifying core modules and their interactions, and ensuring concurrency management. Here's an outline of the design:
System Architecture
MVC or MVVM Pattern: Use Model-View-Controller (MVC) or Model-View-ViewModel (MVVM) design pattern to separate concerns and facilitate testability and maintainability.
Background Task Management: Utilize iOS background task capabilities such as URLSession, BackgroundTasks, and OperationQueue for handling concurrent operations efficiently.
Persistence: CoreData or Realm can be used for local storage to keep track of video tasks' states (e.g., pending, in-progress, completed, failed).
Notifications and Delegates: Use notifications and delegate patterns to communicate between components.
Core Modules
- Task Manager: Central manager to handle all tasks.
- Download Manager: Handles video download tasks.
- Upload Manager: Manages video upload tasks.
- Task Queue: Manages the order and concurrency of tasks.
- Persistence Layer: Stores task states and information.
- UI Layer: Displays task status and controls to the user.
Detailed Design
TaskManager
Responsibilities:
- Coordinate between different task managers (download, upload, delete).
- Maintain a list of current tasks and their statuses.
- Provide methods to start, cancel, pause, and resume tasks.
Key Methods:
startTask(task: VideoTask)cancelTask(task: VideoTask)deleteTask(task: VideoTask)uploadTask(task: VideoTask)pauseTask(task: VideoTask)resumeTask(task: VideoTask)
DownloadManager
Responsibilities:
- Handle the downloading of videos using
URLSession. - Manage download progress and states.
Key Methods:
startDownload(video: Video)pauseDownload(video: Video)resumeDownload(video: Video)cancelDownload(video: Video)
UploadManager
Responsibilities:
- Handle the uploading of videos.
- Manage upload progress and states.
Key Methods:
startUpload(video: Video)pauseUpload(video: Video)resumeUpload(video: Video)cancelUpload(video: Video)
TaskQueue
Responsibilities:
- Manage the execution order of tasks.
- Ensure tasks are executed concurrently within allowed limits.
Key Methods:
addTask(task: VideoTask)removeTask(task: VideoTask)prioritizeTask(task: VideoTask)executeNextTask()
PersistenceLayer
Responsibilities:
- Persist task information and state.
- Retrieve task information on app launch.
Key Methods:
saveTaskState(task: VideoTask)loadTaskStates() -> [VideoTask]
UILayer
Responsibilities:
- Provide a user interface to display task statuses.
- Allow user interactions to start, pause, resume, cancel, and delete tasks.
Components:
- Task List View: Displays all tasks with their current status.
- Task Detail View: Shows detailed progress and options for a selected task.
Concurrency Management
- URLSession: For download and upload tasks, use
URLSession with background configurations to handle tasks even when the app is in the background. - OperationQueue: Use
OperationQueue for managing concurrent execution of tasks with dependencies. - DispatchQueue: Utilize
DispatchQueue for thread-safe operations on shared resources.
Example Workflow
User Initiates a Download:
- The user selects a video to download.
TaskManager creates a VideoTask and adds it to TaskQueue.DownloadManager starts the download using URLSession.PersistenceLayer saves the task state.
User Cancels a Task:
- The user selects a task to cancel.
TaskManager invokes cancelTask on the respective manager (DownloadManager or UploadManager).- The manager cancels the operation and updates the task state.
TaskQueue is updated to remove the task.
App Launches:
PersistenceLayer loads task states.TaskManager reinitializes tasks based on their last known state.
Key Considerations
- Error Handling: Ensure robust error handling and retries for failed tasks.
- Data Consistency: Maintain consistency between the UI, task manager, and persistence layer.
- User Feedback: Provide clear feedback to users about the status of their tasks, especially for long-running operations.
This high-level design outlines the major components and their interactions to create a robust task manager for handling multiple video operations simultaneously on iOS.
Creating a comprehensive example of the entire system is quite extensive, but I'll provide a simplified yet practical example focusing on key aspects: downloading videos using URLSession, managing tasks with OperationQueue, and providing real-time updates with a delegate pattern.
Step 1: Define Models
First, define the VideoTask model to represent tasks.
import Foundation
enum TaskState {
case pending, inProgress, completed, failed, cancelled
}
struct VideoTask {
let id: String
let url: URL
var state: TaskState
var progress: Float
}
Step 2: Task Manager
Next, create the TaskManager to manage video tasks.
import Foundation
class TaskManager {
static let shared = TaskManager()
private var tasks: [String: VideoTask] = [:]
private let queue = OperationQueue()
weak var delegate: TaskManagerDelegate?
private init() {
queue.maxConcurrentOperationCount = 3
loadTasks()
}
func startDownload(task: VideoTask) {
var task = task
task.state = .inProgress
tasks[task.id] = task
PersistenceManager.shared.saveTask(task)
let operation = DownloadOperation(task: task) { [weak self] updatedTask in
self?.tasks[updatedTask.id] = updatedTask
PersistenceManager.shared.updateTask(updatedTask)
self?.delegate?.taskDidUpdate(updatedTask)
}
queue.addOperation(operation)
}
func cancelTask(taskId: String) {
if var task = tasks[taskId] {
task.state = .cancelled
PersistenceManager.shared.updateTask(task)
queue.operations
.compactMap { $0 as? DownloadOperation }
.first { $0.task.id == taskId }?
.cancel()
delegate?.taskDidUpdate(task)
}
}
func getTask(taskId: String) -> VideoTask? {
return tasks[taskId]
}
func getAllTasks() -> [VideoTask] {
return Array(tasks.values)
}
private func loadTasks() {
let savedTasks = PersistenceManager.shared.fetchTasks()
for task in savedTasks {
tasks[task.id] = task
if task.state == .inProgress {
startDownload(task: task)
}
}
}
}
Step 3: Download Operation
Create a custom Operation for downloading videos.
import Foundation
class DownloadOperation: Operation {
private var task: VideoTask
private let updateHandler: (VideoTask) -> Void
private var urlSession: URLSessionDataTask?
init(task: VideoTask, updateHandler: @escaping (VideoTask) -> Void) {
self.task = task
self.updateHandler = updateHandler
}
override func main() {
guard !isCancelled else {
task.state = .cancelled
updateHandler(task)
return
}
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
urlSession = session.dataTask(with: task.url)
urlSession?.resume()
task.state = .inProgress
updateHandler(task)
while !isCancelled && task.state == .inProgress {
Thread.sleep(forTimeInterval: 1)
}
if isCancelled {
task.state = .cancelled
updateHandler(task)
}
}
override func cancel() {
super.cancel()
urlSession?.cancel()
}
}
extension DownloadOperation: URLSessionDataDelegate {
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
task.progress += Float(data.count) / Float(dataTask.countOfBytesExpectedToReceive)
updateHandler(task)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print("Download failed: \(error.localizedDescription)")
self.task.state = .failed
} else {
self.task.state = .completed
}
updateHandler(self.task)
}
}
Step 4: UI Layer
Lastly, implement a simple UI layer to display and manage tasks. Here's a basic view controller using UITableView to list the tasks.
import UIKit
class TaskViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
private var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView(frame: view.bounds)
tableView.delegate = self
tableView.dataSource = self
view.addSubview(tableView)
TaskManager.shared.delegate = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return TaskManager.shared.getAllTasks().count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
let task = TaskManager.shared.getAllTasks()[indexPath.row]
cell.textLabel?.text = task.url.lastPathComponent
cell.detailTextLabel?.text = "\(task.state) - \(Int(task.progress * 100))%"
return cell
}
}
extension TaskViewController: TaskManagerDelegate {
func taskDidUpdate(_ task: VideoTask) {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
Adding a Task
To add a download task, you could add a button that triggers the following:
let videoURL = URL(string: "https://example.com/video.mp4")!
let newTask = VideoTask(id: UUID().uuidString, url: videoURL, state: .pending, progress: 0.0)
TaskManager.shared.startDownload(task: newTask)
To include a persistence layer, we'll use CoreData to store the states of tasks so that they can be restored when the app is relaunched. This involves defining a CoreData model, creating the persistence manager, and updating the TaskManager to interact with the persistence layer.
Step 5: Setting Up CoreData
First, create a new CoreData model (.xcdatamodeld) file and define an entity VideoTaskEntity with the following attributes:
id (String)url (String)state (String)progress (Float)
Step 2: Persistence Manager
Create a persistence manager to handle saving and loading tasks.
import CoreData
import UIKit
class PersistenceManager {
static let shared = PersistenceManager()
private init() {}
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "VideoTaskModel")
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Unresolved error \(error)")
}
}
return container
}()
var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
func saveContext() {
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
func fetchTasks() -> [VideoTask] {
let request: NSFetchRequest<VideoTaskEntity> = VideoTaskEntity.fetchRequest()
do {
let result = try context.fetch(request)
return result.map { VideoTask(id: $0.id!, url: URL(string: $0.url!)!, state: TaskState(rawValue: $0.state!)!, progress: $0.progress) }
} catch {
print("Failed to fetch tasks: \(error)")
return []
}
}
func saveTask(_ task: VideoTask) {
let entity = VideoTaskEntity(context: context)
entity.id = task.id
entity.url = task.url.absoluteString
entity.state = task.state.rawValue
entity.progress = task.progress
saveContext()
}
func updateTask(_ task: VideoTask) {
let request: NSFetchRequest<VideoTaskEntity> = VideoTaskEntity.fetchRequest()
request.predicate = NSPredicate(format: "id == %@", task.id)
do {
let result = try context.fetch(request)
if let entity = result.first {
entity.state = task.state.rawValue
entity.progress = task.progress
saveContext()
}
} catch {
print("Failed to update task: \(error)")
}
}
func deleteTask(_ task: VideoTask) {
let request: NSFetchRequest<VideoTaskEntity> = VideoTaskEntity.fetchRequest()
request.predicate = NSPredicate(format: "id == %@", task.id)
do {
let result = try context.fetch(request)
if let entity = result.first {
context.delete(entity)
saveContext()
}
} catch {
print("Failed to delete task: \(error)")
}
}
}
Conclusion
This example demonstrates the basics of managing video tasks in an iOS app. The TaskManager handles the coordination, DownloadOperation manages the downloading process, and the view controller displays task status updates in real-time. To expand this further, you could add similar structures for uploading and deleting videos, enhance error handling, and improve UI feedback. With the addition of the PersistenceManager, the task manager can now save and load tasks, maintaining state across app launches. This example integrates CoreData to persist task information, ensuring continuity of tasks and progress tracking even after the app is closed and reopened.