Swift Closures with Completion handler

Dhaval Kansara
7 min readMar 16, 2020

--

Closures are self-contained blocks of functionality that can be passed around and used in your code.

Said differently, a closure is a block of code that you can assign to a variable. You can then pass it around in your code, for instance to another function. The function then calls the closure and executes its code, as if the closure is an ordinary function.

Topics what you’ll learn:

  1. How A Closure Works
  2. How Closure Types Works
  3. Closures And Type Inference
  4. Capturing And The Capture List
  5. Closures In Action: The Completion Handler

1. How A Closure Works

Let’s look at an analogy:

  • Bob gives Alice an instruction: “wave your hands!”. Alice hears the instruction and waves her hands. The hand-waving is a function.
  • Alice writes her age on a piece of paper and gives it to Bob. The piece of paper is a variable.
  • Bob writes “Wave your hands!” on a piece of paper and gives it to Alice. Alice reads the instruction on the piece of paper and waves her hands. The instruction, as passed on the piece of paper, is a closure.

Syntax Of Closure:

Swift Language Guide Document

Let’s Go through an example:

let birthday:(String) -> () = { name in
print("Happy birthday, \(name)!")
}
birthday("Bob") // Happy birthday, Bob!

Let’s understand the above code line by line:

  • We’re declaring the closure on the first line, then assign it to the constant birthday, and call the closure on the last line.
  • The closure now has one parameter of type String. It’s declared within the closure type (String) -> ().
  • You can then use the parameter name within the closure. When calling the closure, you provide a value for the parameter.
  • The closure type (String) -> ()
  • The closure expression { name in ... }
  • The closure call birthday(...)

Note: Unlike Swift functions, the parameters of closure aren’t named. When you declare a closure you can specify the types of parameters it has, such as String in the above example, but you don’t specify a parameter name.

2. How Closure Types Works

  • Every closure has a type, just like variables.
let closureName:(ParameterTypes) -> ReturnType = {(parameterName:ParameterType) in}

A closure with no parameters and no return type has the following closure type: () -> ()

Let’s go through an example:

let findMaxNumber:(Int,Int)->(Int) = {(number1:Int,number2:Int)->(Int) in
return number1 > number2 ? number1 : number2
}
let maxNumber = findMaxNumber(10,20)
print(maxNumber) // 20

Let’s understand the above code line by line:

  • The closure findMaxNumber has two parameters, both of type Int and it returns a value of typeInt. number1 and number2 are the local parameter for the of the closure and type offindMaxNumber is (Int,Int)->Int.
  • When the closure is called, it is provided two arguments of type Int, and its return value is assigned to maxNumber, and then printed out.

3. Closures And Type Inference

Swift has a super useful feature called type inference. When you don’t explicitly specify the type of a variable, Swift can figure out on its own what the type of that variable is. It does so based on the context of your code. So type inference and closures go hand-in-hand. As a result, you can leave out many parts of a closure expression. Swift will (usually) infer those parts on its own.

Let’s understand with help of an example:

let numbers = [11, 5, 42, 99, 65, 70, 22]
let sortedNumbers = numbers.sorted(by: <)
print(sortedNumbers) // [5, 11, 22, 42, 65, 70, 99]
  • In the above code we’re creating an array with random numbers, then sorting those number and assigning the result to sortedNumbers.
  • The key part of this example is names.sorted(by: <). That first parameter by: takes a closure.

Now, let’s understand the above closure in different ways.

A) First, when we expand < it becomes this:

names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 < s2 })
  • Above example uses the complete closure syntax, including two parameter names s1 and s2 of type String, and a return type of Bool. It also uses the in and return keywords.

B) The above closure is the same as…

names.sorted(by: { s1, s2 in return s1 < s2 } )ORnames.sorted(by: { s1, s2 in s1 < s2 } )
  • The above example omits the closure parameters because they can be inferred from context. Since we’re sorting an array of Int, those two parameters are inferred as Int.

C) With shorthand arguments…

names.sorted(by: { $0 < $1 } )
  • This example uses the $0 and $1 shorthand to reference the first and second parameters of the closure. The parameters are there, they just don’t have names! Again, with inferred Int types.

D) With trailing closure syntax:

names.sorted { $0 < $1 }
  • This example uses trailing closure syntax. When a closure is the last (or only) parameter of a function, you can write the closure outside the function’s parentheses.

E) And finally, you can also just use the < operator as a function:

names.sorted(by: <)
  • And the last example uses the smaller-than operator < as the closure. In Swift, operators are top-level functions. Its type is (lhs: (), rhs: ()) -> Bool, and that fits the type of sorted(by:) perfectly!

Remember that it’s not always smartest to choose the most concise programming approach. In fact, it helps to be more descriptive than clever!

4. Capturing And The Capture List

A closure can capture constants and variables from the surrounding context in which it is defined. The closure can then refer to and modify the values of those constants and variables from within its body, even if the original scope that defined the constants and variables no longer exist.

In Swift, the simplest form of a closure that can capture values is a nested function, written within the body of another function. A nested function can capture any of its outer function’s arguments and can also capture any constants and variables defined within the outer function.

func makeIncrementer(forIncrement amount: Int) -> () -> Int {let runningTotal = 0let incrementer : () -> Int = {
return runningTotal + amount
}

return incrementer
}let incrementByTwenty = makeIncrementer(forIncrement: 20)
print(incrementByTwenty()) // 20

In the above example makeIncrementer the function accepts one argument as Int and returns a function closure with() -> Int type. This means that it returns a closure, rather than a simple value. The function it returns has no parameters and returns an Int value each time it is called.

Here amount is argument, runningTotal is declared as variable and initialized with 0. Closureincrementer captures amount and runningTotal from the surrounding context.

The name “closure” comes from “enclosing”, and when we stretch it, it comes from the functional programming concept of “closing over”. In Swift, a closure captures variables and constants from its surrounding scope.

If you assign a closure to a property of a class instance, and the closure captures that instance by referring to the instance or its members, you will create a strong reference cycle between the closure and the instance. Swift uses capture lists to break these strong reference cycles. For more information, see Strong Reference Cycles for Closures.

  • Let’s understand with help of an example:
class Database {
var data = 0
}
let database = Database()
database.data = 11010101
let calculate = { [weak database] multiplier in
return database!.data * multiplier
}
let result = calculate(2)
print(result) // 22020202

Here’s what happens in the code:

  • First, you define a class Database. It has one property data. It’s a fictional class, so imagine that it’s super memory intensive…
  • Then, you create an instance of Database, assign it to database, and set its data property to some integer value.
  • Then, you define a closure calculate. The closure takes one argument multiplier, and it captures database. Within the closure, the data is simply multiplied by multiplier.
  • Finally, the closure is called with an argument 2 and its result is assigned to result.

Conclusion: Variables and constants are captured with a strong reference by default, which can cause a strong reference cycle. So, you can break the strong reference cycle with a capture list, by explicitly marking captured values as weak and unowned.

5. Closures In Action: The Completion Handler

Closures are incredibly powerful tools, but if you can’t put them to use, they won’t do you much good.

A common application of a closure is the completion handler. You can refer to the Completion Handler with @escaping and @nonescaping closures.

That’s all for this particular post. I hope I was able to explain Swift closures. You can also learn the completion handler from Completion Handler with @escaping and @nonescaping closures.

Feel Free to post your comments if you face any issues. You can 👏 if you liked the article. Do you know that you can 👏 50 times in a single post? try it.

Thanks for reading…!!!

Dhaval Kansara
iOS | Flutter

--

--