Asynchronous programming in Swift often involves using completionHandler
within functions to handle events. While this approach is common, there are potential pitfalls that developers need to be mindful of. One such pitfall is that there is no guarantee all code paths will eventually call the completionHandler
. This means if we forget to call it, the code will still build successfully, leading to unexpected behaviors or bugs.
To create safer code that ensures the completionHandler
is always called, we can follow a simple two-step process:
Declare a constant using the
let
keyword to store the result.Use a
defer
statement to call thecompletionHandler
.
The defer
statement is a powerful tool in Swift. It ensures that a block of code is executed when the current scope exits, which in this case is just before the function returns. This guarantees that the completionHandler
will always be called, even if an error occurs during the function's execution.
Here's an example of how you can implement this in your code:
import Foundation
func fetchData(url: URL, _ completion: @escaping (Result<Data, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
let result: Result<Data, Error>
defer {
completion(result)
}
guard error == nil else {
result = .failure(error!)
return
}
guard let data else {
result = .failure(NetworkError.noData)
return
}
result = .success(data)
}
.resume()
}
In the above code, we first declare a constant result
to store the outcome of our network call. We then use a defer
statement to call the completionHandler
with the result
just before the function exits. This ensures that regardless of the path our code takes, our completionHandler
is always called.
By incorporating these steps, we can create a safer and more reliable asynchronous code that guarantees our completionHandler
is always executed.
Thanks for Reading! ✌️
If you have any questions or corrections, please leave a comment below or contact me via my LinkedIn account Pham Trung Huy.
Happy coding 🍻