Swift
Background Thread
Multithreading
iOS Development
Concurrency

How to use background thread in swift?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

In Swift, background work exists to keep the main thread free for UI updates and user interaction. The practical answer today is to choose the tool that matches the style of your code: DispatchQueue for simple background execution, OperationQueue for more structured task graphs, and Swift concurrency for modern async workflows.

Keep Heavy Work Off the Main Thread

The point of a background thread is not to use threads for their own sake. The point is to avoid blocking the main thread with work such as file I/O, image processing, parsing large responses, expensive calculations, or long-running synchronous APIs.

If that work happens directly on the main thread, the app can stop responding smoothly to taps, scrolling, and animations. The rule is simple:

  • do UI work on the main thread
  • do heavy or waiting work somewhere else

Using DispatchQueue

DispatchQueue is still one of the most common ways to run work in the background.

swift
1import UIKit
2
3func generateThumbnail(imageView: UIImageView) {
4    DispatchQueue.global(qos: .userInitiated).async {
5        let image = UIImage(named: "large_photo")
6
7        DispatchQueue.main.async {
8            imageView.image = image
9        }
10    }
11}

The pattern has two parts:

  • 'DispatchQueue.global(...) runs the work off the main thread'
  • 'DispatchQueue.main.async switches back for UI updates'

The quality-of-service level tells the system how urgent the work is. Common choices are .userInitiated for work the user is waiting on now, .utility for longer-running tasks, and .background for lower-priority work.

Using Swift Concurrency

In newer Swift code, async or await and Task often produce a cleaner result than manual queue hopping. If the work is already asynchronous, you usually do not need to wrap it inside another background queue.

swift
1import Foundation
2
3func loadText() async throws -> String {
4    let url = URL(string: "https://example.com/data.txt")!
5    let (data, _) = try await URLSession.shared.data(from: url)
6    return String(decoding: data, as: UTF8.self)
7}

Then call it from UI-facing code:

swift
1import Foundation
2
3@MainActor
4final class ViewModel: ObservableObject {
5    @Published var text = ""
6
7    func refresh() {
8        Task {
9            do {
10                text = try await loadText()
11            } catch {
12                text = "Failed to load"
13            }
14        }
15    }
16}

This still keeps the UI responsive. The network request suspends asynchronously instead of blocking the main thread.

Using OperationQueue

OperationQueue is useful when background work has dependencies, cancellation, or more structure than one detached block.

swift
1import Foundation
2
3let queue = OperationQueue()
4queue.maxConcurrentOperationCount = 2
5
6queue.addOperation {
7    let result = expensiveComputation()
8
9    OperationQueue.main.addOperation {
10        print(result)
11    }
12}

This is more overhead than DispatchQueue, but it becomes valuable when tasks must be coordinated explicitly.

Choosing the Right Tool

A practical rule of thumb is:

  • use DispatchQueue for simple background blocks
  • use OperationQueue when tasks depend on one another or need explicit cancellation
  • use async or await and Task when the work is naturally asynchronous

Swift apps can use more than one of these tools. The right answer depends on whether you are integrating with older APIs or writing modern async code.

Returning to the Main Thread Still Matters

Background execution is only half of the pattern. As soon as you need to update UIKit or SwiftUI state, return to the main thread or main actor.

With GCD:

swift
DispatchQueue.main.async {
    label.text = "Done"
}

With Swift concurrency:

swift
await MainActor.run {
    self.statusText = "Done"
}

If you skip that boundary, you risk UI bugs and race conditions.

Avoid Extra Queues Around Already Async APIs

One common mistake is wrapping an already asynchronous API in an extra background queue even though it is not blocking to begin with. For example, URLSession already performs its waiting asynchronously, so adding a global queue around an async network call usually adds noise, not value.

The better question is whether the operation blocks the caller. If it already suspends cleanly, use the async API directly.

Common Pitfalls

One common mistake is doing heavy work on the main thread and only noticing after the UI becomes sluggish.

Another pitfall is forgetting to return to the main thread or main actor before touching UI state.

A third issue is using DispatchQueue.global() for everything even when the code would be clearer as a normal async function and Task.

Finally, do not confuse "off the main thread" with true iOS background execution while the app is suspended. That is a different platform feature and uses different APIs.

Summary

  • Keep heavy work off the main thread so the UI stays responsive.
  • Use DispatchQueue for simple background work and OperationQueue for more structured task management.
  • Prefer async or await and Task in modern Swift code when the work is naturally asynchronous.
  • Return to the main thread or main actor before updating UI state.
  • Choose the concurrency tool that matches the shape of the work instead of forcing one pattern everywhere.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.