Completion Handlers as an Alternative to Delegation

The delegation design pattern is a familiar pattern to Cocoa programmers but there are times when it seems cumbersome to create and use. In this post I look at completion handlers as an alternative to delegation. At first glance they are simpler but come with some caveats.

Lessons from the Apple API’s

I find it useful to look at how Apple does things when learning new techniques. The NSURLSession (or URLSession if you are using Swift) class allows a rich set of interactions with session and data tasks using a number of delegate protocols.

Let’s create a simple example of a class that searches a remote web service. The API for my SearchEngine class is simple with a single search method. With the delegation pattern I have my class adopt the URLSession protocols I need and set the delegate to self when I initialize the session:

class SearchEngine: NSObject, URLSessionTaskDelegate, URLSessionDataDelegate {
  func search(query: String) {

    // ... setup code

    let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
    let task = session.dataTask(with: request)
    task.resume()
  }
}

I can then choose from a number of delegate methods to allow me to interact with the session:

// URLSessionDataDelegate methods
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
  // got some data
}

// URLSessionTaskDelegate methods
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
  // task finished
}

This is easy to understand but sometimes is more than we need. If I have no need to interact with the delegate and just want back the result (or an error) there is a simpler way.

The URLSession API also supports a simpler interface that does not use a delegate. Instead you give a completion handler with the request. The framework calls the completion handler when it has finished the request with the data, response and error code as optional parameters:

class SearchEngineNoDelegate {
  func search(query: String) {
    // ... setup code omitted ....
    let task = session.dataTask(with: request, completionHandler: {
      (data, response, error) in
      // Check response and do something
      // with data or handle error
    })
    task.resume()
  }
}

Note that I have shown the above code snippets in Swift but you can apply the exact same approach with Objective-C blocks replacing Swift closures.

Creating Our Own Completion Handler API

We can follow the example from Apple to add a completion handler to our search engine API to return the search result to the caller. I will give my completion handler two parameters:

  • result: An array of String items that are the result of the search.
  • error: An Error indicating the cause of a failure, otherwise nil.

Our search method with the added completion handler now looks like this:

func search(query: String, completionHandler: ([String], Error?) -> Void) {
  // ... setup code omitted ...
  var error: Error?
  var result = [String]()
  
  // Perform the search operation and
  // set result and error depending
  // on success of the operation
  
  // Pass the result and error back to the caller
  completionHandler(result, error)
}

The calling code, perhaps in a view controller might look like this:

class SearchViewController: UIViewController {
  var searchTitles = [String]()
  let searchEngine = SearchEngine()

  func search(title: String) {
    searchEngine.search(query: title, completionHandler: {
      (result, error) in
      if error != nil {
        self.searchTitles = result
      }
    })
  }
}

Watch Those Retain Cycles

Using a block or closure as a completion handler looks a lot simpler than delegation but it comes with some caveats. The downside is that it can be much harder to understand how memory is being managed.

With the delegation pattern we make sure our delegating object has only a weak reference to the delegate. A completion handler has by default a strong reference to the objects it references which can lead to retain cycles if we are not careful. (See this discussion on capture lists for ways to avoid capturing self).

In the code snippets above the SearchViewController keeps a strong reference to the SearchEngine object but the reverse is not true. The SearchEngine object does not keep a reference to the completion handler and the search method calls the completion handler before it returns. The completion handler never “escapes” from the scope of the search method. This is tricky and easy to get wrong (hint the memory debugger can help). I will save the discussion of escaping closures and capture lists for another time but see the links below for more details.

Further Reading

For a recap on using delegation take a look at my earlier post on defining Swift delegates:

For a deeper discussion on the joy of escaping closures and the changes in Swift 3 see this post by Ole Begemann: