Why background threads are a pain for every iOS developer?
\n\nImagine: you are writing an app to download photos from the internet. Everything works perfectly... until the user presses the "Download" button. The screen freezes, the app lags, and after a couple of seconds, iOS simply kills your process. Sound familiar?
\n\nThe problem is that all operations run on the main thread by default. And the main thread is responsible for rendering the UI — if you block it with a long task, the interface will stop responding to touches. The solution? Use a background thread in Swift.
\n\nIn this article, we will figure out how to work with background threads using real examples. You will learn about GCD, OperationQueue, and the new async/await from Swift 5.5+. Let's go!
\n\nWhat is a thread in Swift?
\n\nA thread is a lightweight process that executes program code. An iOS app has a main thread and many background threads. The main thread handles the UI: updates the screen, processes gestures, animations. Everything related to the interface must happen only on the main thread.
\n\nBackground threads are needed for:
\n- \n
- Downloading data from the network \n
- Working with files (reading/writing) \n
- Complex computations (image processing, encryption) \n
- Working with databases \n
If you try to update the UI from a background thread, the app will crash with an error, or you will see strange interface behavior.
\n\nGCD (Grand Central Dispatch) — the classic way
\n\nGrand Central Dispatch (GCD) is a low-level library for thread management built into iOS/macOS. It works through queues: you put a task in a queue, and the system itself decides which thread to execute it on.
\n\nDispatchQueue.main — the main thread
\nIf you need to execute code on the main thread (for example, to update a UILabel), use:
\n\nDispatchQueue.main.async {\n self.label.text = "Loading complete!"\n}\n\nDispatchQueue.global() — background thread
\nFor background tasks, use the global queue:
\n\nDispatchQueue.global(qos: .background).async {\n // Heavy work\n let imageData = try? Data(contentsOf: url)\n \n // Return to the main thread for UI\n DispatchQueue.main.async {\n self.imageView.image = UIImage(data: imageData!)\n }\n}\n\nThe qos (Quality of Service) parameter determines the priority of the task:
- \n
.userInteractive— highest priority, for UI \n .userInitiated— user is waiting for the result \n .utility— long tasks (file downloads) \n .background— low priority (synchronization, backups) \n
OperationQueue — more control
\n\nOperationQueue is an abstraction over GCD with additional capabilities:
\n- \n
- Cancelling tasks \n
- Dependencies between tasks \n
- Maximum number of concurrent threads \n
Example of downloading multiple images:
\n\nlet queue = OperationQueue()\nqueue.maxConcurrentOperationCount = 2 // No more than 2 tasks at a time\n\nlet operation1 = BlockOperation {\n // Downloading the first image\n sleep(2)\n print("Photo 1 downloaded")\n}\n\nlet operation2 = BlockOperation {\n // Downloading the second image\n sleep(3)\n print("Photo 2 downloaded")\n}\n\n// operation2 will start only after operation1 completes\noperation2.addDependency(operation1)\n\nqueue.addOperations([operation1, operation2], waitUntilFinished: false)\n\nThis is convenient when you need to execute tasks in a strict order or with resource constraints.
\n\nModern async/await (Swift 5.5+)
\n\nStarting with Swift 5.5, Ap