DEV Community

Cover image for Swift Closures Explained: A Comprehensive Guide for iOS Developers
Bugfender
Bugfender

Posted on • Originally published at bugfender.com on

Swift Closures Explained: A Comprehensive Guide for iOS Developers

Introduction

Closures provide a powerful, flexible way for iOS developers to define and use functions in Swift, replacing the blocks used in its predecessor Objective-C. They provide self-contained modules of functionality that you can move around in your code, similar to the lambdas found in other programming languages.

Crucially, closures can capture and store references to any constants and variables from the context in which they’re defined. This is known as ’closing’ over those constants and variables. Swift handles all of the memory management for you.

It’s crucial that Apple developers understand Swift closures as a basis for developing applications in Swift. And in this tutorial, we’re going to really dig down and get into the detail.

Take a look at the essential iOS libraries every iOS developer needs to know right now. They are all in our blog post.


Top 10 iOS Libraries of 2023: Stay Ahead of the Game

Understanding Swift Closures

Swift’s closure expressions have a clean, clear style, with optimizations that encourage brief, clutter-free syntax in common scenarios.

Syntax and basic structure of a Swift closure

The syntax of Swift closures follows a specific pattern. Here’s an example:

{ (params) -> ReturnType in
    // Closure body
    // Perform some operations and return a value (if required)
}


// A simple closure without parameters
let simpleClosure = {
    print("This is a simple closure.")
}

// Closure accepts two Int values and return sum of both variables
let addClosure: (Int, Int) -> Int = { (a, b) in
    return a + b
}

//Closure that multiply two integers 
let multiplyClosure: (Int, Int) -> Int = { $0 * $1 }

// Closure that has no params and return a Integer value
let randomClosure: () -> Int = {
    return Int.random(in: 1...10)
}

Enter fullscreen mode Exit fullscreen mode

Closures can be assigned to variables or constants, passed as arguments to functions, returned from functions, and more. As we can see a Swift closure will have a return type.

Different types of closures in Swift

Global Functions and Global Closures

Global functions are closures that have a name and don’t capture any values.

// Global function (no capturing of values)
func globalFunction() {
    print("Hello, world!, It's a GLobal function!")
}

// Calling the global function
globalFunction()

Enter fullscreen mode Exit fullscreen mode

Similarly, a global closure does not capture any value from its context.

// Global closure 
var closure: () -> Void = {
    let name = "Bugfender"
    print("Hello, \\(name)!")
}

// Calling the global closure
closure()

Enter fullscreen mode Exit fullscreen mode

Now let’s go a little deeper. Here, we’ll look at a global closure that captures the variable name. This closure prints a greeting using the captured variable.

let name = "Bugfender"

// Global closure 
var closure: () -> Void = {
    print("Hello, \\\\(name)!")
}

// Calling the global closure
closure()

Enter fullscreen mode Exit fullscreen mode

A Swift closure can be assigned to a variable, and can be called or passed as an argument to functions.

Nested Functions

Nested functions are closures that have a name and can capture values from their enclosing function, like this:

func mainFunction() {
    let mainValue = 10

    // Nested function
    func internalFunction(x: Int) -> Int {
        return x + outerValue
    }

    let result = internalFunction(x: 5)
    print("Result: \\(result)")
}

mainFunction()

Enter fullscreen mode Exit fullscreen mode

Here, the mainFunction() is calling an internal function to get the result. The internalFunction() is adding the main value to the passed variable value.

Closure Expressions

Closure expressions are unnamed closures written in a lightweight syntax that can capture values from their surrounding context. This can apply to any anonymous function defined within your code.

// Define an array of integers
let numberArray = [6, 4, 9, 7, 8, 11]

// let us sort this array using closure expression
let sortedArray = numberArray.sorted(by: { (a, b) -> Bool in
    return a < b
})

// print the sortedArray
print(sortedArray)

Enter fullscreen mode Exit fullscreen mode

Here, the sorted(by: ) method uses this closure expression to sort the numberArray in ascending order.

Swift is stacked with multiple options to work with date and time object. Check this post to become an expert on dealing with dates and time in Swift.


Mastering Swift Date Operations: A Comprehensive Guide for iOS Developers

Working with Closures in Swift

Now we’ve defined the main types of Closures, let’s explore how we can apply them in our day-to-day work as iOS developers.

Passing closures as parameters to functions

Closures can be passed as parameters to functions. This is a great and flexible feature that allows you to encapsulate behaviour and pass it as an argument to another function.

Here is an example:

// Define a function to take a closure as a parameter
func performSomeOperation(a: Int, b: Int, operation: (Int, Int) -> Int) -> Int {
    let result = operation(a, b)
    return result
}

// Define closure expressions for addition and subtraction
let additionClosure: (Int, Int) -> Int = { (a, b) in
    return a + b
}

let subtractionClosure: (Int, Int) -> Int = { (a, b) in
    return a - b
}

// Call the function with different closures
let result1 = performSomeOperation(a: 5, b: 3, operation: additionClosure) 
// Result: 8
let result2 = performSomeOperation(a: 10, b: 2, operation: subtractionClosure) 
// Result: 8

// You can also pass a closure directly as a parameter
let result3 = performSomeOperation(a: 4, b: 2, operation: { (a, b) in
    return a * b
}) 
// Result: 8

print("Result 1: \\(result1)")
print("Result 2: \\(result2)")
print("Result 3: \\(result3)")

Enter fullscreen mode Exit fullscreen mode

Returning closures from functions

The closures can also be returned as a type by the functions. This gives you the flexibility to create and customize the results as needed.

// Define function that returns a closure
func multiplyFactor(factor: Int) -> (Int) -> Int {
    // Define and return a closure
    let multiplier: (Int) -> Int = { number in
        return number * factor
    }
    return multiplier
}

// Use the function to create specific multiplier closures
let double = multiplyFactor(factor: 2)
let triple = multiplyFactor(factor: 3)

// Use the generated closures to perform multiplication
let result1 = double(10) 
let result2 = triple(10) 

print("Result 1: \\(result1)") // Returns 20
print("Result 2: \\(result2)") // Returns 30

Enter fullscreen mode Exit fullscreen mode

Capturing values and variables in closures

Closures offer the capacity to capture and store references to variables and constants from their surrounding context. This allows closures to capture and remember the values of these variables, even if the enclosing scope has exited.


func incrementFunction(value: Int) -> () -> Int {
    var total = 0
    let incrementer: () -> Int = {
        total += incrementAmount
        return total
    }
    return incrementer
}

let incrementFive = incrementFunction(value: 5)
let result1 = incrementFive()
let result2 = incrementFive()

print("Result 1: \\(result1)") // Returns 5
print("Result 2: \\(result2)") // Returns 10

Enter fullscreen mode Exit fullscreen mode

Escaping Closures and Non-Escaping Closures

In Swift, Closures can be categorised as Escaping and Non-Escaping based on your specific needs and their particular lifetime.

For asynchronous behavior, we use Escaping closures. These can be used when you want a closure to be executed after the current function has finished its execution, making them suitable for scenarios like network requests or animations.

Non-escaping closures are suitable for synchronous operations, and their execution is guaranteed to occur within the function’s scope. This makes them ideal when the result of a closure is required to perform further operations, such as fetching records from local databases to manage pagination.

Difference between a escaping closure and a non-escaping closure

The main difference between escaping and non-escaping closures is based on their behaviour and lifetime, and how they can be executed. Let’s look at these one by one, with examples:

Escaping closures

  • A escaping closure outlive the scope in which it is defined and can be stored for later execution, or used in asynchronous contexts.
  • They are used for asynchronous operations, and their execution can take place after the enclosing function has returned.
  • If we are creating escaping closures, it’s important to mark them with @escaping in function signatures.
// Function defining with escaping closures
func fetchRecords(completion: @escaping (Result<String, Error>) -> Void) {
    DispatchQueue.global().async {
        //any asynchronous network request
        if let data = networkRequest() {
            completion(.success(data))
        } else {
            completion(.failure(NetworkError()))
        }
    }
}

//call the function, that will return the result with received from the network
fetchRecords { result in
    switch result {
        case .success(let data):
            print("Data: \\(data)")
        case .failure(let error):
            print("Error: \\(error)")
    }
}

Enter fullscreen mode Exit fullscreen mode

Non-Escaping closures

  • Non-escaping closures are executed within the scope of the calling function and do not outlive that function call. Their execution is guaranteed to occur synchronously during the function’s execution.
  • They are used for synchronous operations, where the closure execution must take place within the current function’s scope.
  • There is no need to explicitly mark them as ‘non-escaping’. In fact, they are considered non-escaping by default.
//Function to sort the even numbers
func sortedEvenNumbers(_ numbers: [Int], completion: (Result<[Int], Error>) -> Void) {
    let evenNumbers = numbers.filter { $0 % 2 == 0 }.sorted()
    completion(.success(evenNumbers))
}

//Call functions to get sorted even numbers, it will work synchronously
sortedEvenNumbers([1, 2, 3, 4, 5]) { result in
    switch result {
        case .success(let sortedEven):
            print("Sorted even numbers: \\(sortedEven)")
        case .failure(let error):
            print("Error: \\(error)")
    }
}

Enter fullscreen mode Exit fullscreen mode

Handling memory management and potential retain cycles in closures

Avoiding potential retain cycles is crucial to preventing memory leaks and maintaining a healthy application.

Retain cycles occur when objects reference one another in a way that creates a reference loop, preventing them from being deallocated when they are no longer needed.

Here are some the best practices to handle memory management and prevent retain cycles in closures:

Weak and Unowned References

Use weak when the referenced object can become nil, and use unowned when the referenced object will never become nil during the closure’s lifetime.

//Declare a closure
var closure: (() -> Void)?

    init() {
        self.closure = { [weak self] in 
                        // Use [unowned self] for unowned reference
            // Use 'self?' to avoid retain cycle
            self?.callMyMethod()
        }
    }

    func callMyMethod() {
        // Do something
    }

Enter fullscreen mode Exit fullscreen mode

Capture Lists

You can use capture lists to specify how variables or constants should be captured in closures. This allows you to explicitly declare whether a reference should be captured strongly or weakly.

func someFunction() {
    var capturedVar = SomeClass()

    let closure: () -> Void = { [weak capturedVar] in
        capturedVar?.someMethod()
    }

    // Use the closure
}

Enter fullscreen mode Exit fullscreen mode

Use Unowned Self or Weak Self for Retaining Self

If you need to capture self in a closure, consider using unowned self or weak self to break retain cycles. If self becomes nil, it can crash the application, so you need to be cautious here.

    var someClosure: (() -> Void)?

    func setup() {
        someClosure = { [weak self] in
            guard let self = self else { return }
            self.doSomething()
        }
    }

    func doSomething() {
        // Perform your action here
    }

Enter fullscreen mode Exit fullscreen mode

Release the Closure

To prevent a closure from retaining objects, set it to nil when it’s no longer needed.

var someClosure: (() -> Void)? = { [weak self] in
    // Do something using self
}

//release the closure when you don't need it
myClosure = nil

Enter fullscreen mode Exit fullscreen mode

Explore the power of GraphQL for iOS development and discover how it can revolutionize your app-building process


Building Efficient Data-Driven Apps: A GraphQL Tutorial for iOS App Developers

Trailing Closures in Swift

Introduction to trailing closures

Trailing closures allows you to write clean and more readable code when working with functions that take closures as their last argument.

This form of closure is often used in Swift to make code more concise and easy to read, especially for functions that have a closure as their final parameter.

Trailing closures have two main characteristics:

  1. They are used when a closure is the last argument to a function. This means you can move the closure outside the function’s parentheses and thereby enhance code readability.
  2. When a function takes a trailing closure, you can omit the parentheses when calling the function.

Now let’s discuss the benefits and usage of trailing closures in greater depth.

Benefits and usage of trailing closures in Swift functions

As we’ve discussed, trailing closures enhances code readability, something we all appreciate as devs – particularly when we work in teams and have to share code while working on a common project.

By placing the closure outside the function’s parentheses, you create a more concise and visually pleasing code structure.

let numbers = [1, 2, 3, 4, 5]
let mapped = numbers.map { $0 * 2 }

Enter fullscreen mode Exit fullscreen mode

These are often used in APIs to create a declarative interface. This design pattern makes it clear how different operations are chained together.

UIView.animate(withDuration: 0.3) {
    // Write the animation code
}

Enter fullscreen mode Exit fullscreen mode

Removing the parentheses when calling a function with a trailing closure simplifies the syntax and reduces visual noise. Thus trailing closures enhance the overall code structure, and they are easier to compose and extend in terms of functionality.

let filteredList = numbers.filter { $0.isEven }

let result = performSomeOperation(a: 5, b: 3) { (a, b) in
    return a + b
}

Enter fullscreen mode Exit fullscreen mode

Trailing closures work very well with high-order functions like map and filter, making it clear what the closure is doing in the context of the function.

let mapped = numbers.map { $0 * 2 }
let filtered = numbers.filter { $0.isEven }

Enter fullscreen mode Exit fullscreen mode

Swift Closures FAQ

What are Swift Closures?

Swift Closures are self-contained blocks of functionality that you can pass around and use in your code. Similar to lambdas in other languages, they can capture and store references to constants and variables from their context.

How do Swift Closures compare to Objective-C blocks?

Swift closures are similar to Objective-C blocks but offer a cleaner, more flexible syntax. They replace blocks in Swift and provide better memory management and a more concise way to write inline code.

Can Swift Closures capture values?

Yes, Swift closures can capture and store references to any constants and variables from the context in which they’re defined. This feature is known as ‘closing’ over these constants and variables.

What is a trailing closure in Swift?

A trailing closure is a closure expression that’s written after the parentheses of a function call it supports, improving readability, especially when the closure is long or complex.

How do you pass a closure as a parameter in Swift?

You can pass a closure as a parameter by defining a function with a closure as its parameter type. This allows for flexible and dynamic functionality within your functions.

Can you return a closure from a function in Swift?

Yes, in Swift, a closure can be returned from a function. This allows for dynamic creation of functionality that can be customized and used later.

To Sum Up

In this article, we’ve covered various aspects of Swift closures, including their syntax and basic structure, different types of closures, passing closures as parameters to functions, returning closures from functions, capturing values and variables in closures, and the difference between escaping and non-escaping closures. We’ve also discussed how to handle memory management and prevent retain cycles in closures and introduced the concept of trailing closures and their benefits.

By using Swift closures effectively, you’re able to define and use self-contained blocks of code, enhancing its overall expressiveness and flexibility. So by understanding the different types of closures available, and their usage, you can write more modular, readable, and efficient Swift code.

Top comments (0)