Managing Sync and Async Tasks in iOS

func fetchSomeJSON(completion: @escaping Completion) {

let URLPath = "https://...."

do {
let request = try makeGETRequestJob(witURLPath: URLPath)

performNetworkRequestJob(request: request) { [weak self] data, response, error in

guard let responseData = data else {
completion(.failure(error))
return
}

do {
let comments = try self?.parseResponse(fromData: responseData) ?? []
completion(.success(comments))
} catch let error {
completion(.failure(error))
}

}

} catch let error {
completion(.failure(error))
}

}

}
  • Lack of control - Yes the above implementation is out of control :D. The caller of this class has no control over the request i.e cannot resume, cancel or suspend the request when required.
  • Not Maintainable - If for some reason another task/job such as performing data transformation or calling another JSON API then the function fetchSomeJSON as well as tests (if there were any!) would need to change greatly.
  • Difficult to test - The code structure above makes testing very difficult as mocks become very difficult to apply and you many end up writing workarounds to test your production code.

Introducing Waterfall

You may recall this name from Javascript. You are correct, but don’t turn aways just yet because i am not going to propose a javascript solution but a Swift solution (hopefully with more control and type safety).

Implementing Waterfall

Lets start with the protocols.

public protocol Task {

/// Determines whether the task is running.
var isRunning: Bool { get }

/// Boolean stating if the task was cancelled.
var isCancelled: Bool { get }

/// Start the task
func start()

/// Resume a currently suspended or non-started task.
func resume()

/// Cancels the task.
func cancel()

}
public protocol Tasker: class {

typealias JobType = (TaskResult) throws -> Task

/// Adds a task to to be executed.
///
/// - parameter job: The function to execute.
func add(job: @escaping JobType)
/// Adds all tasks to be executed.
///
/// - parameter jobs: The list of functions to execute.
func add(jobs: [JobType])

}
public struct TaskResult {

/// A block to define handshake from previous and next function
public typealias ContinueWithResultsType = (Result<Any>) -> Void

/// The data object of the previous function
public var userInfo: Any?

/// This is executed to let the waterfall know it has finished its task
public var continueWithResults: ContinueWithResultsType
public init(userInfo: Any?, continueWithResults: @escaping ContinueWithResultsType) {
self.currentTask = currentTask
self.userInfo = userInfo
self.continueWithResults = continueWithResults
}

}
public class Waterfall<T>: Task {

//********** HELPERS *********

public enum TaskError: Error {
case noResult
}

public typealias JobType = Tasker.JobType

public typealias CompletionType = (Result<T>) -> Void

//*****************************


public var isRunning: Bool = false

public var isCancelled: Bool = false

private lazy var jobs: [JobType] = []

private var currentJobTask: Task?

private let userInfo: Any?

private var completionBlock: CompletionType

/// Initialise with a userInfo and completion block.
public init(with userInfo: Any? = nil,
completionBlock: @escaping CompletionType) {

self.userInfo = userInfo
self.completionBlock = completionBlock

}
......
}
public func start() {
guard currentJobTask == nil else {
assertionFailure("Waterfall is already executing, suspended or cancelled")
return
}

isRunning = true

continueBlock(userInfo)
}
public func resume() {

if let current = currentJobTask {
current.resume()
} else {
start()
}
}
public func cancel() {
isRunning = false
isCancelled = true
currentJobTask?.cancel()
}
private func continueBlock(_ userInfo: Any?) -> Void {     let result = TaskResult(currentTask: self,
userInfo: userInfo) { [weak self] currentTaskResult in

switch currentTaskResult {
case .success(let result):

self?.continueBlock(result)
case .failure(let error):

self?.finish(error: error)
}

}

do {

self.currentJobTask = try self.jobs.removeFirst()(result)
self.currentJobTask?.resume()
} catch let error {

self.finish(error: error)
}

}

Example

Now that we are done with the implementation details lets look at how we can use it.

func fetchSomeJSON(completion: @escaping Completion) -> Task {

let URLPath = "https://...."

let waterfallTask = Waterfall(completionBlock: completion)

waterfallTask.add(jobs: [
self.makeGetRequestTask(withURLPath: URLPath),
self.performNetworkRequestTask(),
self.parseResponseTask()
])
return waterfallTask}

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Melvin John

Melvin John

A Software Engineer with a passion for technology. Working as an iOS Developer @BBC