Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
31: Grand Central Dispatch Basics
I want to show you something that happens to every single iOS developer at some point: the "frozen app" mystery. You write a piece of code that works perfectly in your head, you run it on the simulator, and suddenly the entire interface just stops responding. You click a button, nothing happens. You try to scroll, it's locked. The app isn't crashed—it's just... stuck.
The Great UI Freeze
Let's look at a scenario I ran into recently. I was building a simple tool to process a massive array of strings—basically simulating some heavy data parsing. I wrote a function to handle the "processing" and called it directly from a button action. Here is what that looked like:
func processMassiveDataset() {
print("Starting heavy work...")
// Simulate a heavy computation that takes a few seconds
for i in 0...1_000_000_000 {
if i == 1_000_000_000 {
print("Finished heavy work!")
}
}
}
@IBAction func handleButtonTap(_ sender: UIButton) {
processMassiveDataset()
print("This should print immediately after the function returns.")
}
When I ran this, the app completely locked up. I couldn't even see the "Finished heavy work!" print statement for several seconds. Why? Because I was running this loop on the Main Queue. In Swift, the main queue is where all the UI updates happen. If you give the main queue a task that takes five seconds to complete, the main queue can't do anything else—including responding to your touches or redrawing the screen—until that loop finishes. It's like blocking the only exit of a building; nobody gets in or out.
Moving the Weight Off the Main Thread
To fix this, I need to move that heavy work to a different queue. This is where Grand Central Dispatch (GCD) comes in. I don't want to manage threads manually (that's a nightmare); I just want to tell the system, "Hey, run this block of code whenever you have a spare core available in the background."
I'll use DispatchQueue.global(). This gives me access to a shared system queue that runs in the background.
@IBAction func handleButtonTap(_ sender: UIButton) {
DispatchQueue.global(qos: .userInitiated).async {
print("Starting heavy work on a background thread...")
for i in 0...1_000_000_000 {
if i == 1_000_000_000 {
print("Finished heavy work!")
}
}
print("Work complete!")
}
print("This prints immediately now!")
}
Notice a couple of things here. First, I used .async. This tells the program: "Start this task in the background and immediately move on to the next line of code." That's why "This prints immediately now!" appears in the console before the loop even finishes. Second, I used qos: .userInitiated. QoS stands for Quality of Service. It's basically a way of telling the OS how urgent this task is. Since the user is waiting for this result, .userInitiated is appropriate. If this was a background backup, I'd use .background.
The "Background Thread" Trap
Okay, so the app is responsive again. Great. But now I want to tell the user that the work is done. I tried to update a UILabel inside that background block:
DispatchQueue.global(qos: .userInitiated).async {
// ... heavy loop ...
// WRONG: Trying to update UI from background
self.statusLabel.text = "Processing Complete!"
}
As soon as I did this, Xcode screamed at me with a purple warning, and the app behaved erratically. This is the golden rule of iOS development: Never update the UI from a background thread. UI components are not thread-safe. If you try to change a label or push a view controller from a background queue, you're asking for a crash or weird visual glitches.
The Round Trip Pattern
The solution is a pattern you will use thousands of times in your career. You jump to the background to do the heavy lifting, and then you "jump back" to the main queue to update the UI. It looks like a nested set of brackets:
@IBAction func handleButtonTap(_ sender: UIButton) {
self.statusLabel.text = "Processing..."
DispatchQueue.global(qos: .userInitiated).async {
// 1. Do the heavy work here (Background)
for i in 0...1_000_000_000 { }
// 2. Jump back to the main queue for the UI (Main)
DispatchQueue.main.async {
self.statusLabel.text = "Processing Complete!"
print("UI updated on the main thread.")
}
}
}
Think of it as a round trip. DispatchQueue.global().async takes you away from the UI so you don't freeze the screen, and DispatchQueue.main.async brings you back home so you can safely tell the user what happened. It's simple, but if you forget that second jump, your app will eventually crash in production.
📋 Practical Task
Exercise: Building a Non-Blocking Image Filter Simulator
Create a simple ViewController with a UIButton and a UILabel. Your goal is to simulate a "heavy image filter" process that doesn't freeze the app.
- The Setup: Create a function called
applyComplexFilter()that uses asleep(3)call or a massiveforloop to simulate a 3-second delay. - The Action: When the button is tapped, change the label text to "Applying Filter..." and immediately trigger the filter process using Grand Central Dispatch.
- The Requirement: Ensure the
applyComplexFilter()logic runs on a background queue so the app remains responsive (you should be able to click the button again or interact with other elements while it's "processing"). - The Completion: Once the simulated filter is finished, use the main queue to update the label to "Filter Applied Successfully!".
There are no comments for now.