Skip to content

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.

swift
{ (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.

swift
// 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.

swift
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.

swift
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:

swift
// 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.

swift
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 [ ].

swift
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.

swift
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.

swift
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:

swift
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, self safely becomes nil. 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:

swift
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!")
        }
    }
}