Closures and Async Ops in Swift
Closures !?
Closures are blocks of code you can pass around in Swift. They're like functions but lighter and more flexible. Closures let you run code on demand, pass it as a variable, or use it for callbacks.
Uses
Closures make code compact and powerful. They’re used everywhere: to sort lists, handle user actions, or call back when data loads. In Swift, closures help make code easier to read and maintain.
Sure, here’s a more detailed, beginner-friendly explanation:
Syntax and Example
A simple closure example that sorts a list of numbers:
let numbers = [3, 1, 2]
let sortedNumbers = numbers.sorted { $0 < $1 }
print(sortedNumbers) // Output: [1, 2, 3]
Backdrop:
Closure Structure: We’re using a closure inside the
sorted(by:)
method to tell Swift how we want the array to be sorted. Thesorted(by:)
method expects a closure as an argument to determine the sorting logic.Shorthand Argument Names ($0, $1): Swift lets us use
$0
,$1
,$2
, etc. as shorthand names for closure parameters:$0
refers to the first item in the comparison (the first element in the array).$1
refers to the second item in the comparison (the next element in the array).
How It Works:
The closure{ $0 < $1 }
means "compare the first element ($0
) to the second element ($1
). If$0
is less than$1
, keep them in that order; otherwise, swap them." Swift goes through each element in the array, applying this comparison repeatedly until the array is fully sorted.Behind the Scenes: Swift handles all the looping and comparisons internally:
- It compares each pair of numbers in the array.
Based on the result of
$0 < $1
, it arranges them in ascending order.
This shorthand makes closures shorter and more readable by removing the need for named parameters. Instead of writing something like this:
let sortedNumbers = numbers.sorted { (first, second) in
return first < second
}
we can use $0
and $1
directly for a cleaner, more concise version:
let sortedNumbers = numbers.sorted { $0 < $1 }
Using $0
and $1
is perfect for quick, simple closures where parameter names would add unnecessary complexity.
Non-Escaping Closures in Swift
Non-Escaping Closures
Non-escaping closures are closures that run right inside the function they’re called in. They don’t hang around after the function ends, so they’re “non-escaping.”
Use-cases
Non-escaping closures are great for quick, immediate tasks that happen within the function. These are operations that don’t require the closure to be saved or used outside of the function’s scope. When you use a non-escaping closure, Swift knows it will be used right away and doesn’t need to “escape” or outlive the function, which allows Swift to optimize performance.
When Swift knows a closure won’t escape a function, it doesn’t need to retain extra memory or storage to keep the closure around. This helps improve performance and makes the code safer, as there’s no risk of retain cycles (when an object holds a strong reference to itself through a closure). Non-escaping closures are ideal for “in-the-moment” tasks that finish immediately within the function scope.
Example of a Non-Escaping Closure
Let’s break down an example where we use a non-escaping closure to calculate an average score from a list of grades.
func calculateScore(grades: [Int], completion: (Int) -> Void) {
let score = grades.reduce(0, +) / grades.count
completion(score)
}
// Calling the function
calculateScore(grades: [70, 85, 90]) { score in
print("Average score is \(score)") // Output: Average score is 81
}
Backdrop:
Function Structure:
The
calculateScore
function takes two parameters:grades
: an array of integers representing scores.completion
: a closure of type(Int) -> Void
that takes an integer (the calculated score) and returns nothing (Void
).
The
completion
closure allows us to do something with the result after the score is calculated, like printing it or updating the UI.
Non-Escaping Closure in Action:
The
completion
closure is called immediately after the calculation, within the same function scope.This closure doesn’t need to be stored or retained for later use. It’s only needed in the moment, when
completion(score)
is called right after calculatingscore
.
Contrast from Escaping Closures:
Immediate Execution: Since the closure executes immediately, it doesn’t need to “escape” or exist beyond this function. As soon as
completion(score)
is called, the closure’s job is done, and Swift can release any memory related to it right away.Efficiency: Non-escaping closures allow Swift to optimize performance. Swift doesn’t have to hold on to the closure or worry about its lifecycle beyond this function, making it faster and using less memory.
Safety: With non-escaping closures, there’s no risk of retain cycles or memory leaks. Since the closure doesn’t outlive the function, it can’t accidentally hold onto
self
or other objects.
Preferring a Non-Escaping Closure here:
Non-escaping closures are ideal here because the closure’s job is done within the function. There’s no async task, no delay, and no need for the closure to “escape” the function’s scope.
Swift knows the closure won’t be needed outside of
calculateScore
, so it treats it as non-escaping by default. This keeps code simpler and makes it clear that the closure will only be used here, immediately.
Non-Escaping for This Task !?
Since completion
is called right after calculating the score, there’s no need to save it or delay its execution. The closure’s entire purpose is to handle a quick task immediately, within the same function call. This makes non-escaping closures perfect for in-the-moment calculations or operations that don’t require waiting for something else (like a network call or timer).
In other words, non-escaping closures are great for synchronous, instant tasks, where you don’t need the closure after it’s executed.
Escaping Closures in Swift
Escaping Closure
Escaping closures are closures saved for later, like for an async task. They "escape" because they’re called after the function that takes them has returned.
Use-cases
Escaping closures are useful for tasks with delays, like fetching data from the internet. You need @escaping
to make sure the closure is stored and can be called after the function ends.
Example of an Escaping Closure
Let’s dive into an example using an escaping closure to simulate a network request.
func fetchProfile(completion: @escaping (String) -> Void) {
// Using a timed delay to simulate network delays in real-world situations
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
completion("User profile loaded!")
}
}
fetchProfile { profile in
print(profile) // Output after 2 seconds: "User profile loaded!"
}
Backdrop:
Function Structure:
The function
fetchProfile
takes acompletion
parameter, which is a closure of type(String) -> Void
. This means the closure accepts aString
(the profile data) and returns nothing (Void
).We mark
completion
with@escaping
, allowing it to run later, after thefetchProfile
function itself has finished. Without@escaping
, Swift would expectcompletion
to be used immediately within the function’s scope.
Simulating an Asynchronous Task:
Real network requests don’t return data instantly. Instead, they involve delays as they connect to servers, retrieve information, and send it back. To simulate this delay, we use
DispatchQueue.global().asyncAfter
, which waits 2 seconds before running the completion closure, as if it were responding after receiving results.After the delay, the closure
completion("User profile loaded!")
is called with the profile data as aString
. This simulates the arrival of data from a network call.
The Closure “Escapes” the Function Scope:
The closure doesn’t execute immediately when
fetchProfile
is called. Instead, it “escapes” the function, meaning it lives on and executes after the 2-second delay.Since the closure will execute later, marking it as
@escaping
lets Swift know it won’t complete within the function’s lifecycle, so it keeps the closure in memory until it’s needed.
Example Usage:
When
fetchProfile
is called, we provide a closure that prints the profile data once it’s received.The print statement runs after 2 seconds:
"User profile loaded!"
appears in the console, showing that the completion closure successfully executed after the delay.
Escaping Closures for Asynchronous Tasks !?
Escaping closures are essential for delayed or asynchronous tasks where results aren’t immediately available. In real-world apps, escaping closures are frequently used in network requests, animations, and background operations. They allow us to handle data only when it’s ready, enabling smooth, responsive code that waits for data without blocking the app’s main thread.
This example highlights how escaping closures help manage complex tasks that require time, making them crucial in asynchronous programming.
Completion Handlers with Escaping Closures
Completion Handlers
Completion handlers are escaping closures used to handle async tasks. They let you act on data only after it’s available. For example, updating the UI after a file download completes.
Common Use Cases for Completion Handlers
Use them for network requests, file downloads, or database queries. When the task completes, it “calls back” with data or an error.
Example Using a Completion Handler
In this example, we’ll simulate downloading a file using a completion handler that returns either success or failure. We use Swift’s Result
type to handle both outcomes in a single, clean structure.
enum NetworkError: Error { case fileNotFound }
func downloadFile(completion: @escaping (Result<String, NetworkError>) -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
// Simulate a successful download after a delay
completion(.success("File downloaded successfully"))
}
}
downloadFile { result in
switch result {
case .success(let message):
print(message) // Output: "File downloaded successfully"
case .failure(let error):
print("Error: \(error)")
}
}
Backdrop:
Define Possible Errors with
enum
:- We define an error type called
NetworkError
using anenum
. Here,fileNotFound
is a case representing a situation where the file could not be found. This allows us to specify custom errors for various scenarios.
- We define an error type called
Function Structure with
Result
and@escaping
:The
downloadFile
function takes acompletion
handler that’s an escaping closure.The type
(Result<String, NetworkError>) -> Void
means the closure will return aResult
type:.success(String)
: A successful result containing aString
(the success message)..failure(NetworkError)
: A failure result containing aNetworkError
.
Simulating Asynchronous Download:
We use
DispatchQueue.global().asyncAfter
with a 2-second delay to mimic a network request. In a real-world app, this could represent the time taken to download a file from a remote server.After 2 seconds,
completion(.success("File downloaded successfully"))
is called, passing a.success
result with a success message.
Handling the Result with Completion Handler:
When we call
downloadFile
, we provide a closure to handle theResult
:On Success: If the result is
.success
, it contains aString
message, which we print.On Failure: If the result is
.failure
, it contains aNetworkError
, which we also print.
Here, we’ve simulated only a successful download, but you could replace completion(.success("..."))
with completion(.failure(.fileNotFound))
to test error handling.
Preferring Result
for Asynchronous Tasks
The Result
type makes it easy to handle success and failure in one step. Instead of needing multiple completion handlers or checking optional values, we can use Result
to enforce handling both outcomes explicitly. This approach is safer, more readable, and ideal for tasks where things might go wrong, like network requests.
Advanced Uses for Closures in Swift
Closures in Functional Programming
Closures make functional programming easy in Swift. Use them with map
, filter
, and reduce
to work with data in a clean, readable way.
Functional programming simplifies complex tasks, like transforming data for a report or dashboard.
let numbers = [1, 2, 3]
// Doubling the original numbers
let doubled = numbers.map { $0 * 2 } // [2, 4, 6]
// Filtering Even Numbers from the array
let evenNumbers = numbers.filter { $0 % 2 == 0 } // [2]
Closures with DispatchQueue
for Background Tasks
With closures and DispatchQueue
, you can run tasks in the background and then update the UI.
Example: Downloading a large file without freezing the app, then showing the file contents when done.
DispatchQueue.global().async {
let data = loadHeavyData()
DispatchQueue.main.async {
updateUI(with: data)
}
}
Dependency Injection with Closures
Closures are great for dependency injection. They allow you to pass specific behavior to objects, making code modular and testable.
Example: In tests, you might pass in mock data instead of real data, making it easier to verify behavior without relying on external sources.
class ServiceManager {
let fetchData: () -> String
init(fetchData: @escaping () -> String) {
self.fetchData = fetchData
}
}
Custom Animations with Closures in SwiftUI
With SwiftUI’s withAnimation
closure, you can trigger smooth animations easily.
withAnimation {
self.isExpanded.toggle()
}
For example, this could expand or collapse a menu, making the app feel interactive and polished.
Dynamic Views in SwiftUI Using Closures
Closures are essential for building dynamic views in SwiftUI. They let you pass custom content, making your UI components reusable.
Example 1: A Button with Different Actions
In this example, we define a custom button view that can perform various actions depending on the closure passed to it. The DynamicButton
accepts a title and an action, making it flexible for different behaviors on button press.
struct DynamicButton: View {
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
DynamicButton(title: "Print Hello") {
print("Hello!")
}
DynamicButton(title: "Print Goodbye") {
print("Goodbye!")
}
}
}
}
Explanation:
DynamicButton
accepts atitle
and anaction
closure. Theaction
closure allows us to define different behaviors for each button instance.When you call
DynamicButton
inContentView
, you can specify a custom action. For example:First Button: Prints
"Hello!"
when pressed.Second Button: Prints
"Goodbye!"
when pressed.
This approach keeps DynamicButton
reusable for different actions without changing the button’s layout.
Example 2: A Button with Generic Content and Action
In this example, we’ll create a GenericButton
that not only accepts a custom action but also lets you define the button’s content (text, icon, etc.) using a generic Content
view. This allows even more flexibility, as you can customize both the look and behavior of the button.
struct GenericButton<Content: View>: View {
let action: () -> Void
let content: Content
init(action: @escaping () -> Void, @ViewBuilder content: () -> Content) {
self.action = action
self.content = content()
}
var body: some View {
Button(action: action) {
content
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
GenericButton(action: {
print("Primary Action")
}) {
Text("Primary Action")
}
GenericButton(action: {
print("Secondary Action")
}) {
HStack {
Image(systemName: "star")
Text("Secondary Action")
}
}
}
}
}
Explanation:
GenericButton
: This button accepts anaction
closure and acontent
view (using a generic typeContent
). The@ViewBuilder
allowscontent
to accept multiple SwiftUI views, such asText
,Image
, or anHStack
combining them.Usage in
ContentView
:First Button: Uses a simple
Text
view as content and prints"Primary Action"
when pressed.Second Button: Combines an
Image
andText
in anHStack
, showing an icon next to the button title. When pressed, it prints"Secondary Action"
.
With this structure, GenericButton
becomes highly flexible, allowing you to change both the button’s appearance and its behavior, making it a powerful tool for dynamic, reusable components in SwiftUI.
Memory Management with Closures
When you use closures in Swift, they can sometimes lead to retain cycles if they capture self
. A retain cycle happens when two objects hold strong references to each other, preventing either from being released from memory. In the case of closures, this can cause memory leaks, where objects remain in memory longer than needed.
Retain Cycles with Closures
When a closure is defined within an object (like a class instance), it captures a strong reference to self
by default if you reference self
inside the closure. This is especially common in asynchronous tasks, where the closure might run after a delay, keeping the class instance in memory until the closure completes.
Example of a Retain Cycle
Consider the following example:
func fetchData(completion: @escaping (String) -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
completion("Data fetched")
}
}
class DataFetcher {
func startFetching() {
fetchData { data in
print("Fetched: \(data)")
}
}
}
Backdrop:
startFetching()
callsfetchData
, which takes a completion closure.The closure captures
self
(theDataFetcher
instance) because we referenceself
implicitly inprint("Fetched: \(data)")
.Since
fetchData
is asynchronous, the closure keepsself
alive until it completes after 1 second, which can create a retain cycle if other parts of the code also hold strong references toself
.
Solution: Using [weak self]
to Avoid Retain Cycles
To break the retain cycle, we use [weak self]
in the closure’s capture list. This tells Swift to create a weak reference to self
, meaning the closure won’t prevent self
from being released if it’s no longer needed elsewhere.
Here’s the corrected code:
class DataFetcher {
func startFetching() {
fetchData { [weak self] data in
guard let self = self else { return }
print("Fetched: \(data)")
}
}
}
Working of [weak self]
Weak Reference: By marking
self
as[weak self]
, we makeself
an optional inside the closure. This meansself
may becomenil
if the instance is deallocated before the closure runs.Optional Binding: We use
guard let self = self
to safely unwrapself
. Ifself
is still around, the closure runs as expected. Ifself
has been deallocated, the closure simply exits without executing, avoiding errors or crashes.
Problem is solved by [weak self]
By using [weak self]
, we prevent the closure from holding a strong reference to self
, breaking the retain cycle:
No Retain Cycle:
self
can be deallocated as soon as no other strong references to it remain, even if the closure hasn’t completed.Memory Efficiency: The closure doesn’t “own”
self
, soself
can be released when it’s no longer needed, preventing memory leaks.
TLDR
When working with closures in async tasks, always consider using [weak self]
to prevent retain cycles, especially if self
might be deallocated before the closure completes. This pattern helps keep your code memory-efficient and avoids unnecessary memory leaks in your app.
I hope this guide has shed light on closures and asynchronous operations in Swift—concepts that have become essential tools in my own two-year journey of iOS development. These insights have helped me write cleaner, more efficient code and build dynamic, reusable components, and I’m excited to share them with others who are also navigating Swift’s capabilities.
As you continue to explore closures and async tasks, remember that these techniques are key for tackling complex problems in iOS development.
If you have questions or run into challenges, feel free to reach out at sakshamshrey@gmail.com. I’d love to hear your feedback and help where I can. Happy coding, and best of luck on your journey to mastering Swift !!
Subscribe to my newsletter
Read articles from Saksham Shrey directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Saksham Shrey
Saksham Shrey
Student, learner and a tech enthusiast || likes to discuss all things tech and also abstracts || believes in First-Principles Thinking || ENTJ-A 🍁