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, otherwisenil
.
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: