Swift Closures with Completion handler
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:
- How A Closure Works
- How Closure Types Works
- Closures And Type Inference
- Capturing And The Capture List
- 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:
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 typeInt
and it returns a value of typeInt
.number1
andnumber2
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 tomaxNumber
, 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 parameterby:
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
ands2
of typeString
, and a return type ofBool
. It also uses thein
andreturn
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 asInt
.
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 inferredInt
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 ofsorted(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 = 11010101let 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 propertydata
. It’s a fictional class, so imagine that it’s super memory intensive… - Then, you create an instance of
Database
, assign it todatabase
, and set itsdata
property to some integer value. - Then, you define a closure
calculate
. The closure takes one argumentmultiplier
, and it capturesdatabase
. Within the closure, thedata
is simply multiplied bymultiplier
. - Finally, the closure is called with an argument
2
and its result is assigned toresult
.
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
andunowned
.
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