In my previous post, I wrote about callbacks in Swift, or more specifically using a completion handler to handle the results of a network request. I talked about how closures can be used to compose functions, similar to function composition in algebra. I mentioned the moment of insight that led to my understanding: “a completion handler is a way of returning values.”
Over the past three weeks I’ve been re-learning and solidifying and internalizing that bit of wisdom, so today I want to expand on it a bit.
This is a Weird Idea!
I typically think of a function as a block of code that processes input to return an output. But a closure is a function that can be passed as input into another function. The idea of returning values via the inputs of a closure seems counter-intuitive.
For Example
Here’s how I would normally use a function:
print(“It will take you about \(calculateTimeRemaining(tasksCompleted: tasksArray)) days to complete this course.”)
func calculateTimeRemaining (tasksCompleted: [Task]) -> Int {
// use the array of completed tasks to calculate n
return n
}
In this code, tasksArray
is passed as input to the function calculateTimeRemaining
, which returns a number of days in response. The returned value replaces the function call, so the final output is a sentence: “It will take you about 12 days to complete this course.” (Supposing n comes out to 12 for no particular reason.)
Now let’s look at how the same thing could be accomplished using a closure, providing the output of calculateTimeRemaining
not as output but as input to the closure.
calculateTimeRemaining (tasksCompleted: tasksArray, closure: printResult(number:))
func printResult(_ number: Int) {
print(“It will take you about \(number) days to complete this course.”)
}
func calculateTimeRemaining (tasksCompleted:[Task], closure: (Int) -> Void) {
// use the array of completed tasks to calculate n
closure(n) // Instead of a return call, we call the closure, with n for its input!
}
In this case calculateTimeRemaining
, rather than returning a value, provides input to its closure function and calls that function. Since printResult
was the function we passed in, that’s what gets called, and printResult
uses the input provided by calculateTimeRemaining
(rather than the output as in the previous example) to insert a number into the printed sentence. Whew.
Why Make It Complicated?
As we talked about last time, if we need to await results, as in results fetched from a network request, then a closure is a good way to return the outcome. The network request function can tie up its own thread making the request, then pass its output as input into a completion handler, which will handle the results back on the main thread.
Also, in Swift a function can take as many inputs as needed, but can only return one output value. If you want it to return more than one output, using a closure is a good option because it allows you to use one function’s inputs as another function’s output. (You could also return a tuple, but that's another story for another day.)
Working Backwards
Setting up functions to work together this way can get confusing. I’ve discovered it’s much easier if you work backwards. Here’s an example…
In my current assignment project I wanted to give a title to a map pin dropped by the user onto a map. This turns out to be a lot more involved than it sounds.
I was mucking about in the tasks involved, getting lost and confused. I knew I needed more than one function because the required reverse-geocoding involves a network request. But I was struggling with how to orchestrate the whole mess into a reasonable title and subtitle for my map pin.
Finally I made the above drawing (is this what you call white boarding??) and based on that, I pieced together the following three lines of code:
// Create the new pin
findPlacemark(at: location) {name, placemark in
self.createPinTitles(poiName: name, place: placemark) {title, subtitle in
self.createPin(at: placemark, title: title, subtitle: subtitle)
}
}
These lines call three functions that didn’t actually exist yet at the time. But writing the function calls helped me figure out how one function's output becomes the next function's input.
Note that I used what Swift calls "trailing closure notation," which means the closure I’m passing into a function follows the function call {in a lovely set of curly braces like this.}
I’ve composed two closures here, one inside the other.
So, it’s complicated.
findPlacemark
reverse-geocodes the location and also determines whether it’s near a particular point of interest.
Then createPinTitles
determines the most specific possible title and subtitle for the pin given the geographic information available. Finally, createPin
actually makes the map pin.
When I wrote these three lines, I did it backwards, thinking about what information I needed to create the outcome I wanted. To create a pin I needed a placemark, title, and subtitle. To determine the title and subtitle I needed a placemark and a potential point of interest name. To get the placemark and/or point of interest, I needed to reverse-geocode the location.
Look at the start of each closure:
{name, placemark
{title, subtitle
In each case the parameters listed are outputs of the previous function, serving as inputs to the function that follows. Pretty cool, eh?
Once I wrote down the function calls for my non-existent functions, I could then see how the inputs and outputs should flow from function to function. That made it much easier to write the functions themselves.
In Conclusion
I’m still a rank beginner, and I do not claim to have found the best way to accomplish naming a map pin. I’m sure one day I’ll look back on this and laugh at my wackadoo approach. But I’m telling you two things for sure:
- A closure is an alternative way to return multiple values from a function.
- When planning how to combine functions and closures to accomplish some multi-part task, start with your function calls. Get them working together in a way that makes sense, then write the actual functions.