Greetings, traveler!
When I wrote the first article about async/await in the Swift Concurrency series, I used an example of an image downloader that uses a function to provide images from the Internet. That function handles the operation result with closures.
func loadImage(
url: URL,
_ completion: ((Result<UIImage, Error>) -> Void)?
) {
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data, let image = UIImage(data: data) else {
completion?(.failure(error ?? ImageError.invalidData))
return
}
completion?(.success(image))
}
task.resume()
}
One disadvantage of this approach was that we must remember to call this closure at the end of each scenario we process. If we forget to call the completion block in some cases, the compiler won’t tell us.
func loadImage(
url: URL,
_ completion: ((Result<UIImage, Error>) -> Void)?
) {
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data, let image = UIImage(data: data) else {
// completion?(.failure(error ?? ImageError.invalidData)) ••• ✅ Compiled successfully
return
}
completion?(.success(image))
}
task.resume()
}
This is definitely not a convenient and safe way to handle the operation result. So, I was wondering, is there a safer way to use closures? There is definitely at least one possible solution. Let’s take a look at it.
The defer keyword
The ‘defer’ statement in Swift executes a set of statements just before code execution leaves the current code block. So, if you want to do something regardless of the result of other operations, this keyword may come to the rescue.
Let’s look at the example of uploading images, but now we use the ‘defer’ keyword.
- Declare a let constant, storing the operation result.
- Then, use a ‘defer’ keyword to call the completion block inside the statement.
This statement guarantees that the completion block will be executed before this function returns.
func loadImage(
url: URL,
_ completion: ((Result<UIImage, Error>) -> Void)?
) {
let task = URLSession.shared.dataTask(with: url) { data, _, error in
let result: Result<UIImage, Error>
defer {
completion?(result)
}
guard let data, let image = UIImage(data: data) else {
result = .failure(error ?? ImageError.invalidData)
return
}
result = .success(image)
}
task.resume()
}
Since our result is a let constant, the compiler will only let us proceed if we assign it a value. This is a very convenient and safe approach.
func loadImage(
url: URL,
_ completion: ((Result<UIImage, Error>) -> Void)?
) {
let task = URLSession.shared.dataTask(with: url) { data, _, error in
let result: Result<UIImage, Error> // ❌ Compile error: Constant 'result' used before being initialized
defer {
completion?(result)
}
guard let data, let image = UIImage(data: data) else {
// result = .failure(error ?? ImageError.invalidData)
return
}
result = .success(image)
}
task.resume()
}
Multiple defers
Can you guess what the console will print out?
func printMethod() {
defer {
print("1")
}
defer {
print("2")
}
defer {
print("3")
}
}
The correct answer is:
3
2
1
If multiple defer statements appear in some scope, they will be executed in reverse order.
Value capture
The ‘defer’ statements don’t capture references or current values of variables.
func captureMethod() {
var title = "First title"
defer {
print(title)
}
title = "Second title"
print(title)
}
So, the console will print:
Second title
Second title
Conclusion
There are other ways to use the ‘defer’ keyword. You can use it in many ways, for example, to call layoutIfNeeded() to update the constraints. Or hide the progress indicator after the network operation is executed. Although not very popular, this tool is quite useful. We just have to keep this in mind.
Check out other posts:
- How to change the order of Calendar weekdaySymbols
- Mutex in Swift 6 and Synchronization
- Exploring the take() method for Optionals
- How to display an icon with a title using the Label in SwiftUI
- How to dysplay byte counts in Swift
- How to format date in SwiftUI
- How to use Hierarchical Colors in SwiftUI
- Global actors and Singletons
- How to organize layout with ControlGroup in SwiftUI