Skip to Content
Course content

31: Grand Central Dispatch Basics

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 a sleep(3) call or a massive for loop 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!".
Rating
0 0

There are no comments for now.

to be the first to leave a comment.