Skip to content

Instantly share code, notes, and snippets.

@ollieatkinson
Created January 23, 2017 22:07
Show Gist options
  • Save ollieatkinson/9a3726dbcd55b2e5e1618c5dd2c5c74e to your computer and use it in GitHub Desktop.
Save ollieatkinson/9a3726dbcd55b2e5e1618c5dd2c5c74e to your computer and use it in GitHub Desktop.
Waterfall
//: 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