Weekly Swift articles, podcasts and tips by John Sundell.

Swift clip: Dispatch queues

Published on 03 Feb 2020

In the second episode of Swift Clips, we’ll take a look at the DispatchQueue API, and how we can use it to write concurrent and asynchronous code in Swift.

Sample code

Creating different types of dispatch queues:

let mainQueue = DispatchQueue.main

let globalQueue = DispatchQueue.global()

let customQueue = DispatchQueue(
    label: "com.myapp.queue"
)

let backgroundQueue = DispatchQueue(
    label: "com.myapp.queue.background",
    qos: .background
)

let concurrentQueue = DispatchQueue(
    label: "com.myapp.queue.concurrent",
    attributes: .concurrent
)

An example of performing JSON decoding away from the main thread, using a custom queue:

extension Data {
    func decoded<T: Decodable>(
        as type: T.Type = T.self,
        handler: @escaping (Result<T, Error>) -> Void
    ) {
        let queue = DispatchQueue(label: "com.myapp.decoding")
        let decoder = JSONDecoder()

        queue.async {
            let result = Result {
                try decoder.decode(T.self, from: self)
            }

            handler(result)
        }
    }
}

One problem with the above is that the handler closure will be called on our internal, custom queue — which will be problematic when used from within our UI code (as UI updates must be performed from the app’s main queue):

private extension MessageViewController {
    func handleLoadedJSONData(_ data: Data) {
        data.decoded(as: Message.self) { [weak self] result in
            do {
                try self?.showMessageView(for: result.get())
            } catch {
                self?.showErrorView(for: error)
            }
       }
    }
}

To fix the above issue in a way that doesn’t require us to always handle our JSON decoding results on the main queue, we can enable a resultQueue to be optionally injected when calling our decoded method:

extension Data {
    func decoded<T: Decodable>(
        as type: T.Type = T.self,
        handledOn resultQueue: DispatchQueue = .main,
        handler: @escaping (Result<T, Error>) -> Void
    ) {
        let queue = DispatchQueue(label: "com.myapp.decoding")
        let decoder = JSONDecoder()

        queue.async {
            let result = Result {
                try decoder.decode(T.self, from: self)
            }

            resultQueue.async { handler(result) }
        }
    }
}

Finally, work items enable us to submit work onto a DispatchQueue which can later be cancelled — which is useful when implementing things like request debouncing:

class SearchResultsLoader {
    private let debounceInterval: TimeInterval
    private var pendingRequestWorkItem: DispatchWorkItem?

    ...

    func performSearch(for query: String) {
        pendingRequestWorkItem?.cancel()

        let requestWorkItem = DispatchWorkItem {
            // Perform the request
            ...
        }

        pendingRequestWorkItem = requestWorkItem

        DispatchQueue.main.asyncAfter(
            deadline: .now() + debounceInterval,
            execute: requestWorkItem
        )
   }
}