Avoid Queue Jumping
We are often told that no matter where we end up, we shouldn't forget where we came from. Usually, this applies to people, but it can also apply to code. Specifically, code that runs on a different queue from the queue it was scheduled on.

Queue-jumping 🦘
It's not unusual to see queue-jumping throughout a project:
DispatchQueue.main.async { in
//Update UI
}
Even with the syntax improvements of using GCD introduced in Swift 3, having these GCD calls littered in our code acts as an interruption to its normal flow making the code harder to read, debug and test.
The above queue-jumping code is littered throughout our code because a lot of the 3rd party code we interact with doesn't guarantee the queue on which their callbacks happen. So if that callback is triggering a UI update, then we need to wrap that UI update code in a GCD block, like shown above. Things can get messy very quickly.
However, in the code that we write, we can avoid this queue-jumping by ensuring that the queue any callbacks are called on is known and configurable.
The approach you will see below is similar to the
delegateQueueparameter on theURLSessioninit'er but with a defaulted parameter twist.
Capturing the queue 🕸️
For this example, I'm going to use an OperationQueue to schedule work on a background queue in the form of Operation instances.
Before we get into the code, it's important to note that an operation is executed in parts, with each part being executed on different queues:
- Setup is performed on the caller's queue.
- Execution is performed on the queue the operation was added to.
So we can capture the caller's queue during setup:
class CapturingOperation: Operation {
private let completion: ((_ result: Result<Bool, Error>) -> ())?
private let callbackQueue: OperationQueue?
//MARK: - Init
init(callbackQueue: OperationQueue? = OperationQueue.current, completion: ((_ result: Result<Bool, Error>) -> ())?) {
self.completion = completion
self.callbackQueue = callbackQueue
super.init()
}
}
The above subclass of Operation has two private properties and takes two parameters in it's init'er:
callbackQueueis the queue on which any callbacks will be executed.callbackQueuedefaults to capture the queue on which theCapturingOperationinstance was scheduled; however, this behaviour can be overridden by the user of this class explicitly passing in a queue instance.completionis the closure that will be triggered when this operation's work is completed. It uses theResultenum type that allows us to encapsulate both the success and failure outcomes in one closure.
You may be thinking, "Why use a custom completion closure when
Operationcomes with a completion closure?". Well, first, thecompletionBlockclosure is executed on a random background queue even if we use the GCD queue switching technique above to push it onto a specific, and secondly, thecompletionBlockclosure is very limited in that you can't pass data back through the closure. Due to both of these limitations, I always use a custom closure.
Now that we have our completion closure and callback queue stored as properties, it's time to perform the task that the operation was created for:
class CapturingOperation: Operation {
// Omitted properties and methods
override func main() {
super.main()
//Operation's actual work happens here
let result = Result<Bool, Error>.success(true)
if let callbackQueue = callbackQueue {
callbackQueue.addOperation {
self.completion?(result)
}
} else {
completion?(result)
}
}
}
In the above main method, once the operation's task is complete, the completion closure is triggered. If the callbackQueue is non-nil, the triggering of the completion closure happens on that queue; if it's nil, then the completion closure is triggered directly from the queue that the operation is running on.
With the queue-jumping happening inside the operation itself, we can safely remove any queue-jumping in the code that uses this operation - simplifying multiple call-sites at the expense of making triggering this operation's completion closure more complex. Spock would be proud.
Calling home has never been so easy
This approach makes a sensible assumption that the queue from which you scheduled an operation is the queue that you want that operation to be called back on. By using a defaulted parameter (callbackQueue) in the init'er if that assumption proves to be false, the callback queue can be overridden with a more appropriate queue.
To see this project in action, head over to the repository and clone the project.