Capturing the callback queue

We are often told that no matter where we end up, we shouldn't forget where we came from. The idea is to keep humble and thankful for what we have achieved. Now normally this apples to people but what if we apply the same idea to code. Specifically multi-threaded code. The advantage of this is to make sure that whatever queue/thread the task was triggered on, is the queue that the callback(s) will also be called on.

Don't leave it to chance 🎲

I've seen many projects that are littered with:

DispatchQueue.main.async { in
    //task to be run on the main queue
}

This is because we are returning from a task that was run on a background queue and don't know which queue it will return on, so we force it onto the main queue. This is particularly important if we are working with the UI as the UI can only be manipulated from the main queue. Even with the improvements of using GCD in Swift 3 being much less verbose, having these structures littered in our code acts as an interruption between receiving the response of some operation/task and then using that response (never mind it's also one additional level of indentation).

In an ideal world, we wouldn't need the above statements. Sadly a lot of the code we interact with doesn't guarantee the queue that callbacks/closures will be called on. However in the code that we write we can introduce a practice that can ensure that whatever queue was used to schedule the background task, is the queue that is used when making the callback.

Callback on your terms ☎️

For this example, I'm going to use Operations rather than GCD because for me even with the improvements to the syntax of GCD in Swift 3, Operations still feel more OOP and encourage me to split work into single units following the Single Responsibility Principle.

class CapturingOperation: Operation {

    let completion : ((_ successful: Bool) -> (Void))?
    private let callBackQueue: OperationQueue

    //MARK: - Init

    init(completion: ((_ successful: Bool) -> Void)?) {
        self.completion = completion
        self.callBackQueue = OperationQueue.current!

        super.init()
    }

    // MARK: - Main

    override func main() {
        super.main()

        sleep(2) //This is where this operation's actual work would be

        callBackQueue.addOperation {
            guard let completion = self.completion else {
                return
            }

            completion(true)
        }
    }
}

In the above snippet we have a subclass of Operation which takes a closure parameter in it's init method - completion. It's this closure that we will ensure is called on the correct queue, in order to do that we capture the queue that this operation was init'd on using:

self.callBackQueue = OperationQueue.current!

It's important to note that an Operation subclass really works across two queues:

  • Setup is performed on the caller's queue.
  • Execution is performed on a background queue.

We maybe have a tendency to think of it as only operating on a background queue but as we can see that's not true.

In the main method once the operation's task is complete we callback using:

callBackQueue.addOperation {
    guard let completion = self.completion else {
        return
    }

    completion(true)
}

Here we schedule a closure to be run on the queue that this operation was called/created on.

So the above example works across both main queue -> background queue and also background queue-> background queue however it's important to note that when creating an async operation you need to override some default behaviour to keep that operation alive while it's waiting on the other operation to finish - see my earlier post on the subject.

That's all folks 🐷

That's really all there is to it, by using this technique we can reduce some of the boilerplate queue manipulation code in our projects.

To see this project in action, head over to https://github.com/wibosco/CapturingTheCallbackQueue-Example, this project contains an operation that has completion closure that updates a UILabel with details about the queue it was updated from. To see it really work, in CapturingOperation try calling the completion closure directly without adding it to the callbackQueue and watch your console output for feedback.