Closures
Closures are self-contained blocks of code that do not require the func keyword or a formal name. They can be assigned to variables, passed as arguments to other functions, or returned from other functions. They are similar to "lambdas" or "blocks" in other programming languages.
Closure Syntax (The in keyword)
Closures can take parameters and return values, just like normal functions. Since they lack a name, the entire syntax is written inside the curly braces { }. Swift uses the in keyword to separate the parameters and return type from the actual body of the code.
{ (parameters) -> returnType in
// body of the closure
}Functions vs Closures
While functions and closures are technically the same thing (functions are essentially named closures), they differ in syntax and everyday usage. Most notably, closures do not use external argument labels when you call them.
// A regular named function
func greet(name: String) -> String {
return "Hello, \(name)!"
}
// The same logic written as a closure stored in a variable
// Notice the explicit type annotation: (String) -> String
let greetClosure: (String) -> String = { (name: String) -> String in
return "Hello, \(name)!"
}
// Calling them looks almost identical: the function uses the label 'name', the closure does not
print(greet(name: "Mia")) // Hello, Mia!
print(greetClosure("Mia")) // Hello, Mia!Trailing Closure Syntax
If a closure is the last argument passed to a function, it can be written outside the function's parentheses for a cleaner look.
func downloadImage(url: String, onCompletion: () -> Void) {
onCompletion()
}
downloadImage(url: "piratedev.com/pic.png", onCompletion: {
print("Action performed!")
})
// Same as above, but uses Trailing Closure Syntax
downloadImage(url: "piratedev.com/pic.png") {
print("Action performed!")
}If a function accepts multiple closures, you can omit the argument label for the first one, but use the parameter names as labels for the subsequent closures.
func animate(duration: Double, animations: () -> Void, completion: () -> Void) {
// ...
}
animate(duration: 0.5) {
print("Fading in...")
} completion: {
print("Animation finished!")
}Shorthand Syntax
Swift's compiler is incredibly smart and allows you to strip away unnecessary code. It provides automatic names for the arguments inside a closure: $0 for the first, $1 for the second, and so on. This allows you to omit the parameter list and the in keyword entirely for simple one-liners (Inline Closures).
The following example shows how a full closure gets shortened:
// Full Syntax
let addNumbers: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
return a + b
}
// Types inferred
let addNumbers2: (Int, Int) -> Int = { a, b in
return a + b
}
// Implicit return
let addNumbers3: (Int, Int) -> Int = { a, b in
a + b
}
// Shorthand argument names
let addNumbers4: (Int, Int) -> Int = { $0 + $1 }
// Operator Shorthand
let addNumbers5: (Int, Int) -> Int = +How Operator Shorthand Works
In Swift, standard operators (like +, -, > and <) are actually built-in functions under the hood.
Because the explicit type annotation on the left side of addNumbers5 tells the compiler exactly what kind of function is required (one that takes two Ints and returns an Int), and the addition operator + perfectly matches that exact signature, you can safely pass the operator symbol itself.
Capturing Values
Closures can "capture" (remember and access) variables from the surrounding context where they were created.
By default, Swift captures values by reference. This means the closure modifies the exact same variable that exists outside of it.
var count = 0
let increment = {
count += 1
}
increment()
print(count) // 1 (The original variable was changed)Capture Lists
If you want the closure to capture a frozen copy of the variable exactly as it was at the moment of creation (capture by value), you use a capture list written in square brackets [ ].
var score = 10
let printScore = { [score] in
print("Score is \(score)")
}
score = 20
printScore() // 10 (It captured the old copy)Memory Management: The Retain Cycle
Memory management in Swift relies on counting how many things are holding onto an object. As long as someone is keeping a strong grip on an object, it stays alive in memory.
Imagine two people holding hands so tightly that neither is allowed to leave the room. This exact scenario happens in Swift when a class holds onto a closure, and that closure captures self (the class itself). They are both keeping each other alive forever. This is called a retain cycle or a memory leak.
To fix this, we have to tell the closure to loosen its grip using a capture list.
[weak self] (The Safe Choice)
Using [weak self] tells the closure to hold onto the class loosely. It essentially says, "I will use this class if it is still here, but I will not force it to stay alive."
Because the class might disappear before the closure actually runs, Swift automatically turns self into an Optional inside the closure. You must use a question mark ?. or safely unwrap it before using it.
class NetworkManager {
var data = "Empty"
func fetchData() {
// The closure holds 'self' loosely
performDownload { [weak self] result in
// We use '?.' because self might have been destroyed by the time this finishes
self?.data = result
print("Data updated!")
}
}
}[unowned self] (The Strict Promise)
Using [unowned self] is also a loose grip, but it comes with a strict promise. It tells Swift, "I guarantee this class will still be alive when this closure runs."
Because of this promise, Swift does not make self an Optional. You can use it directly without a question mark. However, if you break that promise and the class is destroyed before the closure runs, your entire app will crash immediately.
class UserProfile {
var name = "Pirate"
func updateProfile() {
// We promise UserProfile will definitely still exist when this runs
loadName { [unowned self] newName in
// No optional unwrapping needed
self.name = newName
}
}
}NOTE
Always use [weak self] as your default choice. It is incredibly safe and prevents crashes. Only reach for [unowned self] when you are absolutely certain the class will outlive the closure and you want to save a few keystrokes on optional unwrapping.
Escaping Closures (@escaping)
By default, closures in Swift are non-escaping. This means the compiler guarantees the closure will finish running before the function ends. Closures passed to higher-order functions like map, filter, and sort are non-escaping (these functions are covered on the next page).
A closure is called escaping when it is passed as an argument to a function but is executed after that function has already returned. This is almost always used in asynchronous tasks like network requests.
You must explicitly write @escaping before the closure type. Swift enforces this because escaping closures can extend the lifetime of objects they capture. Requiring the keyword forces you to acknowledge this explicitly and prompts you to think about capture semantics like [weak self].
Here is an example using a simulated delay to show how it escapes the main function:
import Foundation
func fetchData(completion: @escaping (String) -> Void) {
print("1. Starting the data fetch...")
// Simulating async work — imagine a real network call here
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
// This closure runs long after the fetchData function has already ended
completion("3. Here is your downloaded data!") // called 2 seconds later
}
// The function returns here, but the closure runs 2 seconds later.
print("2. The function has finished running. Waiting for the delay...")
}
// Calling the function
fetchData { message in
print(message)
}
// Console Output:
// 1. Starting the data fetch...
// 2. The function has finished running. Waiting for the delay...
// -- (2 second pause) --
// 3. Here is your downloaded data!The Golden Rule: @escaping + [weak self]
In real-world iOS development, you will almost always see @escaping and [weak self] used together.
Why? Escaping closures are usually used for tasks that take time, like downloading data from the internet. Imagine a user opens a profile screen, the app starts downloading their profile picture (an escaping closure), but the user gets bored and immediately hits the "Back" button to leave the screen.
- Without
[weak self]: The closure stubbornly keeps the entire profile screen alive in memory just so it has somewhere to put the picture when the download finally finishes. This causes a memory leak. - With
[weak self]: The closure checks if the screen still exists. Because the user left,selfsafely becomesnil. The closure realizes the screen is gone, tosses the downloaded picture in the trash, and gracefully finishes without leaking memory.
Here is what that looks like in practice:
class ProfileViewController {
var profileImage: String = "placeholder"
func loadProfile() {
// The closure is escaping, so we MUST use [weak self]
downloadImage(url: "[piratedev.com/avatar](https://piratedev.com/avatar)") { [weak self] downloadedImage in
// We use 'guard let' to safely check if the screen is still alive
guard let aliveSelf = self else {
print("Screen was closed before download finished. Aborting.")
return
}
// If we get here, the user is still on the screen!
aliveSelf.profileImage = downloadedImage
print("Profile picture updated!")
}
}
}