Swift clip: Dispatch queues
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
)
}
}