Have you ever seen some factory method in UIKit and see this strange parameter called completion
or completionHandler
? Usually you put nil
there and leave it alone. These strange things like (Double,Double) -> Bool
things are closures. Closures are functions you use as types, often in parameters of other functions. In this lesson we’ll explore closures and get under the hood to explain what they are and what they are really doing.
Set Up a Playground
Set up a playground in Xcode, the iPad or on the web at the IBM Swift Sandbox. IF using the IBM Swift Sandbox, make sure you have Foundation imported.
import Foundation
The Basics of Closures
Suppose I want to calculate the volume of a deep dish pizza. Add these functions to the playground:
//area of a round pizza func roundArea(radius:Double) -> Double{ return M_PI * radius * radius } //volume of a round pizza func roundVolume(height:Double, radius:Double ) -> Double{ return roundArea(radius: radius) * height }
Add a variable to compute the volume of a pizza of radius 6 and height 2, then print it
var volume = roundVolume(height:2,radius:6) print( String(format:"Round volume %4.2f",volume))
Run this and get the volume from the area on the console. Which is fine if I had only round pizzas. What if I had rectangle, oval or triangle pizzas? I’d have to write two new functions each: one for the area and one for the volume. However in each case I’m only changing the area calculation. What if I could pass just the calculation to the volume function? That’s what a closure is: a way of writing something like a function but assigning it as a type. Add the function to the class.
func volume( height:Double, dim1:Double, dim2:Double, area:(Double,Double) -> Double) -> Double { return area(dim1,dim2) * height }
Look at the parameter area
. I’ve declared it (Double,Double) -> Double
. That looks a lot like a function that returns Double
, and two Double
parameters. That’s what a closure does as a type. A closure gives a skeletal structure like a function you can pass as a parameter. Below the volume
function add the following line of code.
let rectangleArea = { (length:Double,width:Double) -> Double in return length * width }
When I declare the contents of the identifier, I flesh out something like a function inside of curly braces. Instead of func
before the parameters, I put in
after the return type. There are parameters (length:Double,width:Double)
. I have the return type as a Double
. For closures, there must always be a return value. If you don’t have one mark it Void
or ()
.
Since I declared the type in the closure declaration, I often can remove it from the declaration
let rightTriangleArea = {(length,width) -> Double in return length * width / 2.0 } let ovalArea = {(length,width)->Double in return length/2.0 * width/2.0 * M_PI }
It doesn’t work in all cases though. This give an error if you do not specify the type
let roundSliceArea = { (radius:Double, slices:Double) ->Double in return M_PI * radius * radius / slices }
I can use the same volume function and change how I compute the area.
print (volume(height: 2,dim1: 10, dim2: 12, area:rectangleArea)) print (volume(height: 2,dim1: 10,dim2: 12, area:roundSliceArea)) print (volume(height: 2,dim1: 10, dim2: 12, area:rightTriangleArea)) print (volume(height: 2,dim1: 10, dim2: 12, area:ovalArea))
I do have a problem with a circle. I could define it like this in my storyboard:
let circleArea = {(radius)->Double in return radius * radius * M_PI } print (self.volume(height: 2,dim1: 10, dim2: 12, area:circleArea))
This doesn’t work for the circle, because the number of parameters are different. You’d get this error message
Cannot convert value of type '(Double) -> Double' to expected argument type '(Double, Double) -> Double'
You have choices: Use the oval with the same length and width or a placemarker. I tend to choose the place marker. Change circleArea
to add the extra parameter. Fix the above code to this:
let circleArea = {(radius:Double, placeMarker:Double) -> Double in return M_PI * radius * radius } print (volume(height: 2,dim1: 10, dim2: 12, area:circleArea))
Literal Closure in Parameters
You don’t have to assign the closure to a constant. You can write it explictly in the function. For a rectangle area, you can write this in the playground.
volume = volume( height: 2, dim1: 10, dim2: 12, area: {(width,length)->Double in return width * length } ) print(volume)
You’ll find this a very common way of specifying the closure. When the closure is the trailing parameter, you can place it after the function like this:
volume = volume( height: 2, dim1: 10, dim2: 12) {(radius,placemarker)->Double in return M_PI * radius * radius } print(volume)
Closures as Completion Handlers
One very common use of closures is completion handlers. Make a new function like this:
func volume( height:Double, dim1:Double, dim2:Double, completionHandler:(Double)->() ){ let result = dim1 * dim2 * height completionHandler(result) }
Neither the completionHandler
closure nor the volume
function returns anything. Instead the result is the parameter of the completion handler.
Why would anyone do this? The reason is asynchronous processing. There are methods which Apple or the developer don’t want to run in the main thread. If you are querying a database, or opening a file any results appear in a closure. The database query might take some time, and you don’t want the rest of your application to freeze while you wait for the result. Similarly, while loading a file you don’t want to freeze the system waiting to learn if the load was successful.
Instead of return
in these functions, the method runs on another thread at its own pace and lets the main thread go on its merry way. When done, the method calls something like completionHandler
, handler
, or completion
with the results it returns as the completion parameter.
The developer uses the result their implementation of the completion handler. This is why most often closures happen completely within a function call. Usually they are specific to the code around it. For example, the volume
function with the completionHandler
would code like this to save the result to the volume property and output the result.
volume(height: 2,dim1: 10,dim2: 12) {(result)->() in print(result) volume = result //in a class use self resultLabel.text = String(format:"Completion %5.2f") }
Completion handlers often pass off the value to a property in a class, like the code above assigns the value of result
to volume
. If you run the code above within a class, be sure to include the class identifier or self
. volume = result
should be self.volume = result
. The identifiers within a closure have no scope to the class. They must be explicitly stated.
A Real Example of a Completion Handler
As a general rule, you’ll find completion handlers when you don’t know when you will complete a task. One good example of this is presenting alert views.
Set up a new single view project named ClosureDemo, with Swift as the language. Go to the storyboard and drag a label and button.
Set the font on both to Title 1. Select the button. Title the button Alert. Click the alignment icon in the auto layout menu bar. Check Horizontally in Container and Vertically in container. Change Update from to Items of new constraints like this:
Click Add 2 constraints. Select the Label. Title it Results. Click the pin icon in the auto layout toolbar. In the dialog box that appears type 10 then tab, 10 then tab and 10 then tab. Click down to the bottom and Update frames with Items of New Constraints.
Click Add 3 constraints.
Open the assistant editor and control-drag the label to the code. Name the outlet resultsLabel. Control-Drag from the button to the code. Change to an action. Name the action presentAlert.
Close the Assistant editor. Open the Viewcontroller.swift
code.
In the presentAlert
action, add the following code:
@IBAction func presentAlert(_ sender: UIButton) { volume = 240.0 //example of a real completion handler. let alert = UIAlertController( title: "Alert", message: "Volume is \(volume)", preferredStyle: .alert)
Alert actions only activate after the alert shows and the action’s button gets tapped by the user. The UIAlertAction
initializer uses handlers to describe the action when pressed. Add this to the compute action:
let clearAction = UIAlertAction( title: "Clear", style: .destructive, handler: {(action)->() in self.volume = 0 self.resultLabel.text = "Cleared" } )
In clearAction
, the user sets the volume
property to 0 and reflects that in the Label. Again, the closure is independent of the scope of its class. You must specify self
in order to use the ViewController
classes’ properties and methods.
Add two more examples of actions:
let doubleAction = UIAlertAction( title: "Double", style: .default, handler: {(action)->() in self.volume *= 2 self.resultLabel.text = "\(self.volume)" }) let cancelAction = UIAlertAction( title: "Cancel", style: .cancel, handler:{(action)->() in self.resultLabel.text = "\(self.volume)" })
Add the actions to the alert
alert.addAction(clearAction) alert.addAction(doubleAction) alert.addAction(cancelAction)
Finally present the alert controller. present
has a completion handler. Add a print
statement to it to see it in action, then a print
after the present
.
present(alert, animated: true, completion: {()->Void in print ("present Completed") }) print ("Outside Handler")
Build and run. Tap the Alert button. You get the alert. The console printed this:
Outside Handler
present Completed
After calling present
, The system executed the next step, and did not wait for it to finish printing Outside Handler. Once the presentation completed, the system printed present Completed. The alert shows:
Tap the double button. The label reads 240. Try the other buttons.
You’ll find closures in most often in these handlers. Anything that takes and unpredictable amount of time will use a handler in a closure.
The Whole Code
You can find the code for this tutorial on the IBM Swift Sandbox. For cutting and pasting into an Apple Playground on iPad or Xcode, Use the code below.
// // Closure Demo file // For MakeAppPie.com closure tutorial // Sep 2016 by Steven Lipton // // import Foundation //area of a round pizza func roundArea(radius:Double) -> Double{ return M_PI * radius * radius } //volume of a round pizza func roundVolume(height:Double, radius:Double ) -> Double{ return roundArea(radius: radius) * height } var volume = roundVolume(height:2,radius:6) print( String(format:"Round volume %4.2f",volume)) //Closure to change the area formula func volume( height:Double, dim1:Double, dim2:Double, area:(Double,Double) -> Double) -> Double { return area(dim1,dim2) * height } //Assigning type (Double,Double) -> Double to Classes let rectangleArea = { (length:Double,width:Double) -> Double in return length * width } let rightTriangleArea = {(length,width) -> Double in return length * width / 2.0 } let ovalArea = {(length,width)->Double in return length/2.0 * width/2.0 * M_PI } let roundSliceArea = { (radius:Double, slices:Double) ->Double in return M_PI * radius * radius / slices } //Trying out the volume function print (volume(height: 2,dim1: 10, dim2: 12, area:rectangleArea)) print (volume(height: 2,dim1: 10,dim2: 12, area:roundSliceArea)) print (volume(height: 2,dim1: 10, dim2: 12, area:rightTriangleArea)) print (volume(height: 2,dim1: 10, dim2: 12, area:ovalArea)) //Fitting a single parameter formula with a placemarker let circleArea = {(radius:Double, placeMarker:Double) -> Double in return M_PI * radius * radius } print (volume(height: 2,dim1: 10, dim2: 12, area:circleArea)) // closure within parameters volume = volume(height: 2, dim1: 10, dim2: 12, area: {(width,length)->Double in return width * length}) print(volume) // Trailing closure outside parentheses volume = volume(height: 2, dim1: 10, dim2: 12) { (radius,placemarker)->Double in return M_PI * radius * radius } //Closures as Completion Handlers func volume( height:Double,dim1:Double,dim2:Double, completionHandler:(Double)->() ){ let result = dim1 * dim2 * height completionHandler(result) } // For asynchronous processing, use the closure instead of returning the result. volume(height: 2,dim1: 10,dim2: 12) {(result)->() in print(result) volume = result }
The ClosureDemo Alert project
This is the code for the ClosureDemo Alert project. Add the label and button as described above than connect to this code in ViewController.
// // ViewController.swift // ClosurePlay // // Created by Steven Lipton on 9/9/16. // Copyright © 2016 Steven Lipton. All rights reserved. // import UIKit class ViewController: UIViewController { var volume = 0.0 @IBOutlet weak var resultLabel: UILabel! @IBAction func compute(_ sender: UIButton) { volume = 240.0 //example of a real completion handler. let alert = UIAlertController( title: "Alert", message: "Volume is \(volume)", preferredStyle: .alert) let clearAction = UIAlertAction( title: "Clear", style: .destructive, handler: {(action)->() in self.volume = 0 self.resultLabel.text = "\(self.volume)" }) let doubleAction = UIAlertAction( title: "Double", style: .default, handler: {(action)->() in self.volume *= 2 self.resultLabel.text = "\(self.volume)" }) let cancelAction = UIAlertAction( title: "Cancel", style: .cancel, handler:{(action)->() in self.resultLabel.text = "\(self.volume)" }) alert.addAction(clearAction) alert.addAction(doubleAction) alert.addAction(cancelAction) present(alert, animated: true, completion: {()->Void in print ("In present Handler") }) print ("Outside Handler") } override func viewDidLoad() { super.viewDidLoad() } }
Leave a Reply