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

Implementing Waterfall

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

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}

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

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Load balancing an Azure function API for high availability using Azure front door

How to move to #5G?

Bias in Algorithms

Easy Golang projects to finish in a weekend

Python Turtle Graphics and Tkinter GUI Programming — Compucademy

Python strategy game with Turtle Graphics and Tkinter

6 Skills Every Developer Should Have Besides Coding Skills

What is Integration With Magento and How to Develop It?

How to create fast websites with Golang

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

More from Medium

The Solid principles — form an iOS Developers point of view

I finally used method-swizzling

D V N C I — A Storyboardless & Clean & Multidirectional Architecture

Generics and InOut Parameters in Swift with Examples