stratigrafia

COMPLETION CLOSURES IN SWIFT

12/20/2017

Completion closures in Swift are incredibly useful: they let you execute some code after some computationally long task is completed, often a task that takes an indeterminate amount of time to complete. For example, requests for data over the internet are common examples, because the quality of the internet connection may be unpredictably long. For these kinds of jobs, Apple's APIs come with completion handlers. You might also do your own data processing where a completion handler would be useful. The syntax for completion handlers looks odd at first, but the principles are straightforward.

A simple function with a simple closure

It's important to realize that functions involving completion handlers have two parts, the first being the function itself, which handles the computationally intensive part of the work. This is followed by the function call itself, which defines how the completion handler works, that is, what happens after the complex job finishes. For example, consider this function:

func heavyLifting(_ input: String, completion: (String) -> Void) {
  let resultString = input + " and then some"
  completion(resultString)
}

The name is a reminder that this is the function that would do the computationally expensive part of the work, although what this function is doing is not hard: it only concatenates a string. The syntax of this function is the unusual part, as it has two parameters. The first (input) is a string, but the second (completion) looks more complicated. For its type, it takes what is called a function signature. In this case, the signature is: (String) -> Void, meaning that it has one parameter, a String (which need not be named here) and it returns nothing (Void). The final step in the heavyLifting function is calling this completion function, and giving it some results; in this case, I'm passing the results of our (not so) difficult computation.

When heavyLifting is called, we have to define a completion handler and what it will do when it receives the data. The function call of heavyLifting also looks odd at first:

heavyLifting("a string", completion: { (output: String) -> Void in
  print(output)
})

Here, heavyLifting is called with its two arguments, the first being the string and the second being a function with a signature that matches what is expected (one parameter, a string, and a return type of Void). This is followed by 'in', then the contents of the completion handler. In this case, the completion handler will print the string that it receives from heavyLifting, that is, when completion(resultString) is called.

In short, heavyLifting is given a string (input), performs some work on it (calculates resultString), then passes that to completion(). When heavyLifting is called, that string (resultString) is printed.

A more complex closure

The advantage of this approach is that the completion closure could do anything we want. For example, maybe we don't want to show the string. Maybe we just want to print some other message:

heavyLifting("a string", completion: { (output: String) -> Void in
  if output.count > 25 {
    print("a long string")
  } else {
    print("a short string")
  }
})

In this case, our call of heavyLifting will calculate the length of the resultString and print one of two messages, depending on how long it is. In other words, the work inside of heavyLifting doesn't change, but what we do with those results can.

Trailing closures and their simpler syntax

The completion argument in heavyLifting is the last parameter, so it is called a trailing closure. There's a simpler syntax available for trailing closures:

heavyLifting("a string") { (output: String) in
  print(output)
}

Rather than list it as the final parameter, the closing parentheses of the function call is given, followed by a set of curly braces, which contains the entire closure. Furthermore, because our closure returns nothing, we can skip the -> Void part. This makes for a simple syntax, although the various types of simplifications that are possible can make it hard initially to see that all of these closures are doing the same thing.

Stand-alone closures

As closures are just functions, if you are going to call a particular closure multiple times, it can be simpler and shorter to define the closure outside of the function that will call it. For example, if we just wanted to print the result from the closure, we could define a function that has the correct signature ((String) -> Void):

func printResult(output: String) -> Void {
  print(output)
}

Calling it from heavyLifting is now done succinctly:

heavyLifting("Humongous", completion: printResult)

Why this works may not be immmediately obvious since printResult has one parameter (output), but none appears to be assigned here. In this case, the argument for that parameter is assigned when completion is called, from inside of heavyLifting. Again, these kinds of shortcuts make closures easy things to use, but only once you recognize it as a shortcut.

Defining stand-alone closures now requires that we have three parts to the problem: the function that does the difficult work:

func heavyLifting(_ input: String, completion: (String) -> Void) {
  let resultString = input + " and then some"
  completion(resultString)
}

the function that defines a closure:

func stringLength(output: String) -> Void {
  print("This string is \(output.count) characters long")
}

and the call that combines the function with the closure:

heavyLifting("Humongous", completion: stringLength)

Even though this is slightly more complex in that it has three parts, it has the benefit that each of the parts is defined separately, leading to better testability, and that the final invocation of the function is as simple as possible.

 

Home