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.

Instabug

This ad keeps all of Swift by Sundell free for everyone. If you can, please check this sponsor out, as that directly helps support this site:

Instabug

Instabug: Investigate, diagnose and resolve issues up to four times faster. Whether it’s a crash, slow screen transitions, slow network calls or unresponsive UIs, Instabug lets you utilize powerful performance patterns to trace the cause of each issue. Detect if a specific app version, device or network connection is affecting the user experience and spot trends and spikes. Get started now and ship apps that your users will love.

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
        )
   }
}