Most developers don't use dispatch groups on a daily basis. They have a limited use, but I want to show you in this episode that the DispatchGroup class can greatly simplify the code you write when used correctly.

Project Setup

In this episode, we work with a simple project, Takeaway. The project is a work in progress and, as the name implies, it allows users to order takeaway food. The application uses a tab bar controller with two tabs, Home and Profile. The Home tab is blank at the moment. We focus on the Profile tab in this episode.

The Profile tab is populated by the ProfileViewController class. It displays the user's profile and their order history in a table view. Let's take a look at the implementation of the ProfileViewController class. The implementation is simple and straightforward to make sure we can focus on the use of dispatch groups. The project uses the Model-View-Controller pattern, which means the view controller does most of the heavy lifting. It fetches data from a remote API and it manages that data to populate its table view.

The ProfileViewController class uses an instance of the NetworkManager class to fetch the profile and the user's order history from a remote API. Requesting data from the remote API is an asynchronous operation. This is important to understand because it means that the profile view controller doesn't know when it receives a response from the remote API and which request completes first.

The current implementation works, but it isn't perfect. The responses from the remote API are stored in the profile and orders properties. Every time the values of these properties are set, the table view is reloaded.

private var profile: Profile? {
    didSet { tableView.reloadData() }
}

// MARK: -

private var orders: [Order] = [] {
    didSet { tableView.reloadData() }
}

The implementation of the UITableViewDataSource protocol is a bit complex because the profile property is of type Profile?. The table view presents two types of information, the user's profile and the user's order history. Each type is displayed in its own section.

Before we move on, I'd like to show you the implementation of the NetworkManager class. We use the URLSession API to fetch data from the remote API. I added an artificial delay using the sleep(_:) function to make it easier to illustrate the problem we're trying to solve.

func fetchProfile(_ completion: @escaping (ProfileResult) -> Void) {
    URLSession.shared.dataTask(with: URL.profile) { (data, _, error) in
        sleep(2)

        ...
    }.resume()
}
func fetchOrders(_ completion: @escaping (OrdersResult) -> Void) {
    URLSession.shared.dataTask(with: URL.orders) { (data, _, error) in
        sleep(1)

        ...
    }.resume()
}

We take advantage of the Codable protocol and the JSONDecoder class to convert the data the remote API returns to model objects. The Profile and Order structs are lightweight and conform to the Codable protocol.

Simplifying With Dispatch Groups

If you run the application, then it's clear that there's room for improvement. The user experience isn't great. The DispatchGroup class can help us with this. The goal is simple. The application should wait for both requests to complete before presenting any data to the user. I also want to show an activity indicator view as long as the application doesn't have any data to display.

We could implement a solution without dispatch groups by nesting the requests we make to the remote API. This is a technique that is often used by developers unfamiliar with dispatch groups. The downsides are that it's a pain to debug, it complicates unit testing, and it can spin out of control as the number of requests grows.

Another option is working with one or more helper properties to keep track of state. This is a viable option, but as the project grows, this approach starts to loose its appeal. The code can become complex and hard to understand. I always strive to manage as little state as possible in a project, which is one of the reasons I use reactive programming in most projects I work on.

The DispatchGroup class is a fine solution and a good fit for the problem we're trying to solve. We already covered the API in the previous episode. Let's apply what we learned. Navigate to the fetchData() method in the ProfileViewController class. We create an instance of the DispatchGroup class and store a reference in a constant with name group.

private func fetchData() {
    // Create Dispatch Group
    let group = DispatchGroup()

    ...
}

Before we invoke the fetchProfile(_:) method on the NetworkManager instance, we invoke the enter() method on the DispatchGroup instance. We notify the dispatch group that we're about to start a task. In the completion handler that we pass to the fetchProfile(_:) method, we invoke the leave() method on the DispatchGroup instance to indicate that the task completed, successfully or unsuccessfully.

private func fetchData() {
    // Create Dispatch Group
    let group = DispatchGroup()

    // Enter Group
    group.enter()

    // Fetch Profile
    networkManager.fetchProfile { [weak self] (result) in
        switch result {
        case .success(let profile):
            self?.profile = profile
        case .failure(let error):
            print(error)
        }

        // Leave Group
        group.leave()
    }

    // Fetch Orders
    networkManager.fetchOrders { [weak self] (result) in
        switch result {
        case .success(let orders):
            self?.orders = orders
        case .failure(let error):
            print(error)
        }
    }
}

We repeat these steps for the fetchOrders(_:) method. Before we invoke the fetchOrders(_:) method on the NetworkManager instance, we invoke the enter() method on the DispatchGroup instance. In the completion handler that we pass to the fetchOrders(_:) method, we invoke the leave() method on the DispatchGroup instance. This should look familiar.

private func fetchData() {
    // Create Dispatch Group
    let group = DispatchGroup()

    // Enter Group
    group.enter()

    // Fetch Profile
    networkManager.fetchProfile { [weak self] (result) in
        switch result {
        case .success(let profile):
            self?.profile = profile
        case .failure(let error):
            print(error)
        }

        // Leave Group
        group.leave()
    }

    // Enter Group
    group.enter()

    // Fetch Orders
    networkManager.fetchOrders { [weak self] (result) in
        switch result {
        case .success(let orders):
            self?.orders = orders
        case .failure(let error):
            print(error)
        }

        // Leave Group
        group.leave()
    }
}

We won't be using the wait() method in this example. Remember that the wait() method blocks the current thread until the tasks of the dispatch group have completed executing. The fetchData() method is executed on the main thread and we don't want to block the main thread at any point. The notify(queue:execute:) method is a better fit. This is what I have in mind.

We invoke the notify(queue:execute:) method on the DispatchGroup instance, passing a reference to the main dispatch queue as the first argument. Remember that the dispatch queue we pass to the notify(queue:execute:) method is the dispatch queue to which the closure, the second argument, is submitted when the tasks of the dispatch group have completed executing. In the closure we pass to the notify(queue:execute:) method, we reload the table view.

private func fetchData() {
    ...

    group.notify(queue: .main) {
        self.tableView.reloadData()
    }
}

It's no longer necessary to reload the table view when the values of the profile and orders properties are set. We can therefore remove the property observers of the profile and orders properties.

// MARK: -

private var profile: Profile?

// MARK: -

private var orders: [Order] = []

Build and run the application to see the result. This is a good start, but there's still room for improvement.

Improving the User Experience

The user sees an empty table view as long as the requests are in flight. We can improve this user experience with a few small changes. Define an outlet with name activityIndicatorView of type UIActivityIndicatorView!.

import UIKit

class ProfileViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var tableView: UITableView!

    // MARK: -

    @IBOutlet var activityIndicatorView: UIActivityIndicatorView!

    ...

}

Open Main.storyboard and add an activity indicator view to the profile view controller. Center the activity indicator view in the center of its superview. With the activity indicator view selected, open the Attributes Inspector on the right and check the checkbox labeled Hides When Stopped. Select the profile view controller, open the Connections Inspector on the right, and connect the activityIndicatorView outlet to the activity indicator view in the storyboard.

Adding an Activity Indicator View

Revisit ProfileViewController.swift and navigate to the fetchData() method. Before we define the DispatchGroup instance, we hide the table view and invoke startAnimating() on the activity indicator view.

private func fetchData() {
    // Update User Interface
    tableView.isHidden = true
    activityIndicatorView.startAnimating()

    // Create Dispatch Group
    let group = DispatchGroup()

    ...

}

In the closure we pass to the notify(queue:execute:) method, we show the table view and invoke stopAnimating() on the activity indicator view.

private func fetchData() {
    ...

    group.notify(queue: .main) {
        self.tableView.reloadData()
        self.tableView.isHidden = false
        self.activityIndicatorView.stopAnimating()
    }
}

Build and run the application to see the result. This looks much better if you ask me.

Adding an Activity Indicator View

What's Next?

We refactored the implementation of the profile view controller by taking advantage of the DispatchGroup class. I'm sure you agree that the API of the DispatchGroup class isn't complex. With very little effort, we significantly improved the user experience of the Profile tab without overcomplicating the implementation of the ProfileViewController class.

Dispatch groups aren't used very often, but they have their use. It's important to carefully consider the requirements of the project you're working on and decide whether the DispatchGroup class is a good fit.