diff --git a/SYNQueue/SYNQueue/SYNQueue.swift b/SYNQueue/SYNQueue/SYNQueue.swift index 093feeb..aa4fef7 100644 --- a/SYNQueue/SYNQueue/SYNQueue.swift +++ b/SYNQueue/SYNQueue/SYNQueue.swift @@ -67,6 +67,7 @@ public class SYNQueue : NSOperationQueue { let logProvider: SYNQueueLogProvider? var tasksMap = [String: SYNQueueTask]() var taskHandlers = [String: SYNTaskCallback]() + var taskCoalescingHandlers = [String: SYNTaskCoalescingCallback]() let completionBlock: SYNTaskCompleteCallback? public var tasks: [SYNQueueTask] { @@ -120,6 +121,16 @@ public class SYNQueue : NSOperationQueue { taskHandlers[taskType] = taskHandler } + /** + Add a coalescing handler for a task type. This will give an array of tasks to the callback (all of the same type) which must all be completed by the handler. + + :param: taskType The task type for the handler + :param: taskCoalescingHandler The coalescing handler for this particular task type, must be able to take multiple tasks, coalesce them, and complete them all. + */ + public func addTaskCoalescingHandler(taskType: String, taskCoalescingHandler:SYNTaskCoalescingCallback) { + taskCoalescingHandlers[taskType] = taskCoalescingHandler + } + /** Deserializes tasks that were serialized (persisted) */ @@ -144,7 +155,24 @@ public class SYNQueue : NSOperationQueue { :param: op A SYNQueueTask to execute on the queue */ override public func addOperation(op: NSOperation) { - if let task = op as? SYNQueueTask { + if var task = op as? SYNQueueTask { + + // If this task type has coalescing block + // If there are other non-started task in the queue that are of the same type + // Call the task coalescing handler with an array of the other non-started + // tasks of the same task type in the queue + + if let coalescingHandler = taskCoalescingHandlers[task.taskType] { + var tasksToCoalesce = self.tasks.filter({ return ($0.taskType == task.taskType && !$0.executing && $0.taskID != task.taskID && $0.dependencies.count == 0) }) + if tasksToCoalesce.count > 0 { + tasksToCoalesce.append(task) + task = coalescingHandler(tasksToCoalesce) // Overwrite task to the new coalesced task and continue + + // Cancel tasks that have now been coalesced + for task in tasksToCoalesce { task.cancel() } + } + } + if tasksMap[task.taskID] != nil { log(.Warning, "Attempted to add duplicate task \(task.taskID)") return @@ -155,10 +183,10 @@ public class SYNQueue : NSOperationQueue { if let sp = serializationProvider, let queueName = task.queue.name { sp.serializeTask(task, queueName: queueName) } + + op.completionBlock = { self.taskComplete(task) } + super.addOperation(task) } - - op.completionBlock = { self.taskComplete(op) } - super.addOperation(op) } func addDeserializedTask(task: SYNQueueTask) { @@ -172,6 +200,7 @@ public class SYNQueue : NSOperationQueue { } func runTask(task: SYNQueueTask) { + if let handler = taskHandlers[task.taskType] { handler(task) } else { diff --git a/SYNQueue/SYNQueue/SYNQueueTask.swift b/SYNQueue/SYNQueue/SYNQueueTask.swift index 67b16f3..26d5095 100644 --- a/SYNQueue/SYNQueue/SYNQueueTask.swift +++ b/SYNQueue/SYNQueue/SYNQueueTask.swift @@ -9,6 +9,7 @@ import Foundation public typealias SYNTaskCallback = (SYNQueueTask) -> Void +public typealias SYNTaskCoalescingCallback = ([SYNQueueTask]) -> SYNQueueTask public typealias SYNTaskCompleteCallback = (NSError?, SYNQueueTask) -> Void public typealias JSONDictionary = [String: AnyObject?] @@ -196,7 +197,7 @@ public class SYNQueueTask : NSOperation { // Convert the dictionary to an NSDictionary by replacing nil values // with NSNull - var nsdict = NSMutableDictionary(capacity: dict.count) + let nsdict = NSMutableDictionary(capacity: dict.count) for (key, value) in dict { nsdict[key] = value ?? NSNull() } diff --git a/SYNQueueDemo/SYNQueueDemo.xcodeproj/project.pbxproj b/SYNQueueDemo/SYNQueueDemo.xcodeproj/project.pbxproj index 50e0b28..0dfabea 100644 --- a/SYNQueueDemo/SYNQueueDemo.xcodeproj/project.pbxproj +++ b/SYNQueueDemo/SYNQueueDemo.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 9FD63FC51B338BA5001BD09A /* TaskCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD63FC41B338BA5001BD09A /* TaskCell.swift */; }; 9FD63FC71B33EA2F001BD09A /* NSUserDefaultsSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD63FC61B33EA2F001BD09A /* NSUserDefaultsSerializer.swift */; }; 9FD63FCD1B3417DF001BD09A /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD63FCC1B3417DF001BD09A /* Utils.swift */; }; + DC6668E61B8CC7090045852A /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6668E51B8CC7090045852A /* SettingsViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -61,6 +62,7 @@ 9FD63FC41B338BA5001BD09A /* TaskCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskCell.swift; sourceTree = ""; }; 9FD63FC61B33EA2F001BD09A /* NSUserDefaultsSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSUserDefaultsSerializer.swift; sourceTree = ""; }; 9FD63FCC1B3417DF001BD09A /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; + DC6668E51B8CC7090045852A /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -109,6 +111,7 @@ 9FD63F961B333BDC001BD09A /* AppDelegate.swift */, 9F70B5021B368AB500BDB945 /* ConsoleLogger.swift */, 9FD63F981B333BDC001BD09A /* ViewController.swift */, + DC6668E51B8CC7090045852A /* SettingsViewController.swift */, 9FD63F9A1B333BDC001BD09A /* Main.storyboard */, 9FD63F9F1B333BDC001BD09A /* LaunchScreen.xib */, 9FD63FC41B338BA5001BD09A /* TaskCell.swift */, @@ -188,6 +191,7 @@ 9FD63F891B333BDC001BD09A /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 0700; LastUpgradeCheck = 0630; ORGANIZATIONNAME = Syntertainment; TargetAttributes = { @@ -247,6 +251,7 @@ files = ( 9FD63FCD1B3417DF001BD09A /* Utils.swift in Sources */, 9FD63F991B333BDC001BD09A /* ViewController.swift in Sources */, + DC6668E61B8CC7090045852A /* SettingsViewController.swift in Sources */, 9FD63FC51B338BA5001BD09A /* TaskCell.swift in Sources */, 9F70B5031B368AB500BDB945 /* ConsoleLogger.swift in Sources */, 9FD63F971B333BDC001BD09A /* AppDelegate.swift in Sources */, diff --git a/SYNQueueDemo/SYNQueueDemo/Base.lproj/Main.storyboard b/SYNQueueDemo/SYNQueueDemo/Base.lproj/Main.storyboard index 2f2fe12..f514fdd 100644 --- a/SYNQueueDemo/SYNQueueDemo/Base.lproj/Main.storyboard +++ b/SYNQueueDemo/SYNQueueDemo/Base.lproj/Main.storyboard @@ -1,14 +1,14 @@ - + - + - + @@ -17,12 +17,8 @@ - - - - - + @@ -52,7 +48,7 @@ - + + + + + + + + + + + + + + + + + + + + + - - - + + + + - - - - - - - - + + + @@ -145,6 +138,15 @@ + + + + + + + + + @@ -152,6 +154,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SYNQueueDemo/SYNQueueDemo/SettingsViewController.swift b/SYNQueueDemo/SYNQueueDemo/SettingsViewController.swift new file mode 100644 index 0000000..8804632 --- /dev/null +++ b/SYNQueueDemo/SYNQueueDemo/SettingsViewController.swift @@ -0,0 +1,33 @@ +// +// SettingsViewController.swift +// SYNQueueDemo +// +// Created by Sidhant Gandhi on 8/25/15. +// Copyright (c) 2015 Syntertainment. All rights reserved. +// + +import UIKit + +let kAddDependencySettingKey = "settings.addDependency" +let kAutocompleteTaskSettingKey = "settings.autocompleteTask" + +class SettingsViewController: UITableViewController { + + @IBOutlet weak var autocompleteTaskSwitch: UISwitch! + @IBOutlet weak var dependencySwitch: UISwitch! + + override func viewDidLoad() { + super.viewDidLoad() + + self.dependencySwitch.on = NSUserDefaults.standardUserDefaults().boolForKey(kAddDependencySettingKey) + self.autocompleteTaskSwitch.on = NSUserDefaults.standardUserDefaults().boolForKey(kAutocompleteTaskSettingKey) + } + + @IBAction func addDependencySwitchToggled(sender: UISwitch) { + NSUserDefaults.standardUserDefaults().setBool(sender.on, forKey: kAddDependencySettingKey) + } + + @IBAction func autocompleteTaskSwitchToggled(sender: UISwitch) { + NSUserDefaults.standardUserDefaults().setBool(sender.on, forKey: kAutocompleteTaskSettingKey) + } +} diff --git a/SYNQueueDemo/SYNQueueDemo/ViewController.swift b/SYNQueueDemo/SYNQueueDemo/ViewController.swift index 4ee3b11..a6a3187 100644 --- a/SYNQueueDemo/SYNQueueDemo/ViewController.swift +++ b/SYNQueueDemo/SYNQueueDemo/ViewController.swift @@ -16,7 +16,7 @@ class ViewController: UIViewController, UICollectionViewDataSource, UICollection var totalTasksSeen = 0 var nextTaskID = 1 lazy var queue: SYNQueue = { - return SYNQueue(queueName: "myQueue", maxConcurrency: 2, maxRetries: 3, + return SYNQueue(queueName: "myQueue", maxConcurrency: 1, maxRetries: 3, logProvider: ConsoleLogger(), serializationProvider: NSUserDefaultsSerializer(), completionBlock: { [weak self] in self?.taskComplete($0, $1) }) }() @@ -27,6 +27,8 @@ class ViewController: UIViewController, UICollectionViewDataSource, UICollection super.viewDidLoad() queue.addTaskHandler("cellTask", taskHandler: taskHandler) + queue.addTaskCoalescingHandler("cellTask", taskCoalescingHandler: taskCoalescingHandler) + queue.addTaskHandler("cellTaskCoalesced", taskHandler: coalescedTaskHandler) queue.loadSerializedTasks() let taskIDs = queue.operations @@ -49,22 +51,59 @@ class ViewController: UIViewController, UICollectionViewDataSource, UICollection func taskHandler(task: SYNQueueTask) { // NOTE: Tasks are not actually handled here like usual since task - // completion in this example is based on user interaction + // completion in this example is based on user interaction, unless + // we enable the setting for task autocompletion + log(.Info, "Running task \(task.taskID)") - // FIXME: This needs to be a toggle option in the UI - // Set task completion after 5 seconds -// Utils.runOnMainThreadAfterDelay(5, callback: { () -> () in -// task.completed(nil) -// }) - - // Redraw this task to show it as active - if let index = findIndex(queue.operations as! [NSOperation], task) { - runOnMainThread { - let path = NSIndexPath(forItem: index, inSection: 0) - self.collectionView.reloadData() + // Do something with data and call task.completed() when done + // let data = task.data + + // Here, for example, we just auto complete the task + let taskShouldAutocomplete = NSUserDefaults.standardUserDefaults().boolForKey(kAutocompleteTaskSettingKey) + if taskShouldAutocomplete { + // Set task completion after 3 seconds + runOnMainThreadAfterDelay(3, { () -> () in + task.completed(nil) + }) + } + + runOnMainThread { self.collectionView.reloadData() } + } + + func coalescedTaskHandler(task: SYNQueueTask) { + // NOTE: Here we would find a clever way to coalesce the task + + if let taskDataArray = task.data as? Array { + // Do something + } + + // Here, for example, we just auto complete the task + let taskShouldAutocomplete = NSUserDefaults.standardUserDefaults().boolForKey(kAutocompleteTaskSettingKey) + if taskShouldAutocomplete { + // Set task completion after 3 seconds + runOnMainThreadAfterDelay(3, { () -> () in + task.completed(nil) + }) + } + + runOnMainThread { self.collectionView.reloadData() } + } + + func taskCoalescingHandler(tasks: [SYNQueueTask]) -> SYNQueueTask { + + log(.Info, "Coalescing tasks" + ", ".join(tasks.map({ return $0.taskID }))) + + var dataArray = [AnyObject]() + for task in tasks { + if let data: AnyObject = task.data { + dataArray.append(data) } } + + let coalescedTask = SYNQueueTask(queue: queue, taskID: ", ".join(tasks.map({ return $0.taskID })), taskType: "cellTaskCoalesced", dependencyStrs: [], data: dataArray) + + return coalescedTask } func taskComplete(error: NSError?, _ task: SYNQueueTask) { @@ -102,7 +141,8 @@ class ViewController: UIViewController, UICollectionViewDataSource, UICollection if let task = queue.operations[indexPath.item] as? SYNQueueTask { cell.task = task cell.nameLabel.text = "task \(task.taskID)" - if task.executing { + let taskShouldAutocomplete = NSUserDefaults.standardUserDefaults().boolForKey(kAutocompleteTaskSettingKey) + if task.executing && !taskShouldAutocomplete { cell.backgroundColor = UIColor.blueColor() cell.failButton.enabled = true cell.succeedButton.enabled = true @@ -118,28 +158,30 @@ class ViewController: UIViewController, UICollectionViewDataSource, UICollection // MARK: - IBActions - @IBAction func addTapped(sender: UIButton) { + @IBAction func addTapped(sender: UIBarButtonItem) { let taskID1 = nextTaskID++ - let taskID2 = nextTaskID++ let task1 = SYNQueueTask(queue: queue, taskID: String(taskID1), taskType: "cellTask", dependencyStrs: [], data: [:]) - let task2 = SYNQueueTask(queue: queue, taskID: String(taskID2), - taskType: "cellTask", dependencyStrs: [], data: [:]) - let task3 = SYNQueueTask(queue: queue, type: "cellTask") - // Make the first task dependent on the second - task1.addDependency(task2) + let shouldAddDependency = NSUserDefaults.standardUserDefaults().boolForKey(kAddDependencySettingKey) + if shouldAddDependency { + let taskID2 = nextTaskID++ + let task2 = SYNQueueTask(queue: queue, taskID: String(taskID2), + taskType: "cellTask", dependencyStrs: [], data: [:]) + + // Make the first task dependent on the second + task1.addDependency(task2) + queue.addOperation(task2) + } queue.addOperation(task1) - queue.addOperation(task2) - queue.addOperation(task3) totalTasksSeen = max(totalTasksSeen, queue.operationCount) updateProgress() collectionView.reloadData() } - @IBAction func removeTapped(sender: UIButton) { + @IBAction func removeTapped(sender: UIBarButtonItem) { // Find the first task in the list if let task = queue.operations.first as? SYNQueueTask { log(.Info, "Removing task \(task.taskID)")