Created
January 23, 2017 22:07
-
-
Save ollieatkinson/9a3726dbcd55b2e5e1618c5dd2c5c74e to your computer and use it in GitHub Desktop.
Waterfall
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//: Playground - noun: a place where people can play | |
import UIKit | |
protocol Async: class, Task { | |
} | |
extension Waterfall: Async { | |
} | |
/// A wrapper to hold onto the result object | |
public struct AsyncResult { | |
typealias ContinueWithResultsType = (Any?, Error?) -> Void | |
/// The async associated with the result | |
weak var async: Async? | |
/// The data object of the previous function | |
var userInfo: Any? | |
/// This is executed to let the waterfall know it has finished its task | |
var continueWithResults: ContinueWithResultsType | |
} | |
/// A Task represents any syncronus or asyncronus object so we can cancel, suspend and resume. | |
public protocol Task { | |
/// Determines whether the task is running. | |
var isRunning: Bool { get } | |
/// Start the task | |
func start() | |
/// Resume a currently suspended or non-started task. | |
func resume() | |
/// Cancels the task. | |
func cancel() | |
} | |
extension URLSessionTask: Task { | |
/// Determines whether the task is running. | |
public var isRunning: Bool { | |
return state == .running | |
} | |
/// Default implementation calls through to resume. URLSessionTask are suspended by default. | |
public func start() { | |
resume() | |
} | |
} | |
/// Runs the tasks array of functions in series, each passing their results to the next in the array. | |
/// However, if any of the tasks pass an error to their own callback, the next function is not executed, | |
/// and the main callback is immediately called with the error. | |
public class Waterfall: Task { | |
/// The signature for a job task. | |
public typealias JobType = (AsyncResult) throws -> Task | |
/// The information to pass to the first job. | |
var userInfo: Any? | |
/// Boolean stating if the task is running. | |
public var isRunning: Bool = false | |
/// Boolean stating if the task was cancelled. | |
public var isCancelled: Bool = false | |
/// Boolean stating if the task was paused. | |
public var isPaused: Bool = false | |
/// The callback after all the tasks have completed. | |
public var completionBlock: (Waterfall, Any?, Error?) -> Void = { _, _, _ in } | |
/// Resume a currently suspended or non-started task. | |
public func resume() { | |
isPaused = false | |
if let current = current { | |
current.resume() | |
} else { | |
start() | |
} | |
} | |
/// Cancels the current task and does not execute anymore. | |
public func cancel() { | |
isCancelled = true | |
current?.cancel() | |
} | |
/// Start the waterfall | |
public func start() { | |
guard current == nil else { | |
assertionFailure("Waterfall is already executing, suspended or cancelled") | |
return | |
} | |
continueBlock(userInfo) | |
} | |
/// The list of jobs in the waterfall. | |
var jobs: [JobType] = [ ] | |
/// The current executing task. | |
var current: Task? | |
convenience init() { | |
self.init(with: nil) | |
} | |
/// Initialise with a userInfo to pass into the first task. | |
public init(with userInfo: Any?) { | |
self.userInfo = userInfo | |
} | |
/// Adds a single task to the waterfall to be executed. | |
/// | |
/// - parameter job: The function to execute. | |
public func add(job: @escaping JobType) -> Waterfall { | |
jobs.append(job) | |
return self | |
} | |
/// Adds all task to the waterfall to be executed. | |
/// | |
/// - parameter jobs: The list of functions to execute. | |
public func add(jobs: [JobType]) { | |
self.jobs.append(contentsOf: jobs) | |
} | |
private var continueBlock: (Any?) -> Void { | |
return { userInfo in | |
guard !self.isCancelled else { | |
self.isRunning = false | |
return | |
} | |
guard self.jobs.count > 0 else { | |
self.finish(result: userInfo) | |
return | |
} | |
let result = AsyncResult(async: self, userInfo: userInfo) { [weak self] userInfo, error in | |
if let error = error { | |
self?.finish(error: error) | |
} else { | |
self?.continueBlock(userInfo) | |
} | |
} | |
do { | |
self.current = try self.jobs.removeFirst()(result) | |
self.current?.resume() | |
} catch let error { | |
self.finish(error: error) | |
} | |
} | |
} | |
private func finish(result: Any? = nil, error: Error? = nil) { | |
self.isRunning = false | |
completionBlock(self, result, error) | |
} | |
} | |
/// A synchronus task | |
class SyncTask: Task { | |
typealias ExecutionType = (Void) -> Void | |
/// A boolean indicating if the task is running. | |
var isRunning: Bool = false | |
/// A boolean indicating if the task has been cancelled. | |
var isCancelled: Bool = false | |
/// A boolean indicating if the task has been suspended. | |
var isSuspended: Bool = false | |
/// The execution block. | |
var execution: ExecutionType | |
init(execution: @escaping ExecutionType) { | |
self.execution = execution | |
} | |
/// Start the task | |
func start() { | |
guard !isCancelled else { | |
return | |
} | |
isRunning = true | |
execution() | |
isRunning = false | |
} | |
/// Resume a currently suspended or non-started task. | |
func resume() { | |
start() | |
} | |
/// Cancels the task. This stops the sync task executing. | |
func cancel() { | |
isCancelled = true | |
} | |
/// Temporarily suspends a task. This doesnt have any affect on the sync task. | |
func suspend() { | |
isSuspended = true | |
} | |
} | |
let waterfall = Waterfall() | |
waterfall.add { result in | |
SyncTask { | |
result.continueWithResults(nil, nil) | |
} | |
}.add { result in | |
SyncTask { | |
result.continueWithResults(nil, nil) | |
} | |
}.completionBlock = { waterfall, result, error in | |
} | |
waterfall.resume() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment