Dealing with asynchronous tasks is an everyday developer task, and Swift developers mostly use closures for this type of work. Their syntax is clear and expressive, but handling a closure may be error-prone when we don’t adequately represent the results of an asynchronous operation. Result-oriented programming aims to reduce that complexity by providing a simple way to represent these results. This article will go through the basics of closures and show you how to use Result
in your code.
All examples can be found in this Github repository.
Key Takeaways
- Swift closures are self-contained blocks of code that can be passed around and used in your code like any other object; they play a crucial role in handling asynchronous tasks.
- Result-oriented programming in Swift reduces complexity by providing a simple way to represent the results of an asynchronous operation, replacing nil-checks in optionals with a Result
enum. - The Result
enum has two basic cases, success(T) and error(Error), which represent the return value of an operation that has two possible outcomes: successful or unsuccessful. - Result-oriented programming can make writing and handling asynchronous code easier and more intuitive, and is an important tool for any Swift developer.
Closures in Swift
Closures are self-contained blocks of code. They can be passed around and used in your code as any other object. In fact, closures have types, in the same way Int
or a String
are types. What makes them different is that they can have input parameters and they must provide a return type.
By default, closures don’t have any special feature which makes them asynchronous. It’s the way they are used that make them play well in asynchronous environments.
Syntax
A closure is defined by its parameters, return type and statements within it:
{ (parameters) -> (return type) in
(statements)
}
Closure parameter types can be any Swift type. The return type can also be any Swift type. In some cases, the return type is Void
and there’s no need for return
statement.
Let’s take a look at how to implement a simple closure which computes the square of a number. As any regular function, you would have two parameters and one returned value.
let sumNumbers: ((Int, Int) -> Int) = { firstNumber, secondNumber -> Int in
return firstNumber + secondNumber
}
There can be any number of statements inside the closure body, as long as the value of the return type is returned. In this case, the closure returns the sum of firstNumber
and secondNumber
.
Closures can be executed like any method:
sumNumbers(10,4) // returns 14
How closures work in asynchronous environment
If you look at any closure, you will see that it encapsulates a block of code. An instance of a closure can be passed around your code and executed without any knowledge of its internals. That makes them perfect for asynchronous development. A typical use case is when you specify the code that should execute upon the return of an asynchronous operation, and pass it as its parameters. The asynchronous method will do its work and execute code from your closure, no matter what it contains.
Suppose we want to carry out our computation of the square in the background, and execute some arbitrary code that can use its result. We’d pass a closure as our completion
parameter:
func square(of number: Int, completion: @escaping (Int) -> Void)
As you can see square(of number: completion:)
method has two parameters. The first parameter is number
of type Int
which is the number to be squared. The second one is a closure of type (Int) -> Void
. When executing this method, the squared number result will be provided as the Int
parameter in closure. The closure’s return type is Void
since there’s nothing to return. And as it’ll be called after the function returns, it must be prefixed as @escaping
.
Let’s take a look how this function can be implemented:
func square(of number: Int, completion: @escaping (Int) -> Void) {
OperationQueue().addOperation {
let squared = number * number
OperationQueue.main.addOperation {
completion(squared)
}
}
}
Inside the method body, squared
is computed first in the background. After that, completion
closure is called on the main thread. Since completion has Int
as a first parameter, squared
will be passed to that parameter.
square(of: 5, completion: { squaredNumber in
print(squaredNumber) // Prints "25"
})
This image shows how parameters and closure return values are passed around.
Optional parameters and their problems
In some cases, the successful execution of asynchronous work may depend on various variables, such as network connectivity. In those cases, asynchronous calls need to supply information about errors that occurred. The closure also needs to provide results if the call was successful. Since there are many possible outcomes, closures define their parameters as optionals.
For example, the URLSession
API defines the following method for running network requests.
func dataTask(with url: URL,
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
You can see that completionHandler
is a closure with three optional parameters. When working with them, you need to check which are nil
and which aren’t. If Error
is not nil, you need to handle the error. If it is, you need to unwrap the Data
and URLResponse
objects and work with them. But there’s no obvious procedure for covering all possible successful and unsuccessful cases.
Result-oriented programming
Result-oriented programming aims to fix this, by essentially replacing nil-checks in optionals with a Result<T>
enum.
What’s Result<T>
?
Result<T>
is how we represent the return value of an operation that has two possible outcomes: successful or unsuccessful.
This representation is very similar to the way optionals are implemented in Swift. Remember that optimonals can be in one of two states
- Contain a wrapped value – meaning that there is some value in the optional instance.
- Contain nothing – meaning that optional instance contains nothing.
If you look at how optionals are implemented in Swift, you will see that it’s actually implemented as an enum:
enum Optional<T> {
case some(T)
case none
}
Optional
is a generic enum. This enables us to use optionals with any type, like Optional<Int>
or Optional<UIView>
.
Similarly, Result<T>
provides two basic cases, success(T)
and error(Error)
. T
is the type defined by the result, and Error
is the Swift native error definition.
Basic usage of Result<T>
A basic implementation of Result<T>
enum would look like this:
enum Result<T> {
case success(T)
case error(Error)
}
Let’s say that you’re building software that deals with math computations. Some of them can fail, due to incorrect user input. A classic example would be attempting to divide a number by zero (which is not allowed by the fundamental rules of mathematics). So, if you were to build a division method that takes two numbers and divides them, you would have two possible outcomes. Either the division if successful and number is returned, or a division by zero occurred. By using Result<T>
we can express it like this:
// Define error that can occur in the computation process
enum MathError: Error {
case divisionWithZero
}
func divide(_ first: Double, with second: Double) -> Result<Double> {
// Check if divisor is zero
// If it is, return error result
if second == 0 { return .error(MathError.divisionWithZero) }
// Return successful result if second number
// is not zero
return .success(first / second)
}
To then carry out our division, we’d use a switch
statement:
let divisonResult = divide(5, with: 0)
switch divisionResult {
case let .success(value):
print("Result of division is \(value)")
case let .error(error):
print("error: \(error)")
}
Advanced usage of Result<T>
Result<T>
enum can be easily incorporated into any development environment. To understand how to do that, we’ll use an image manipulation example.
In this example, the image located at a URL will be filtered with sepia filter and returned in closure via Result<UIImage>
object. Our method signature would look like this:
func applyFilterToImage(at urlString: String, completion: @escaping ((Result<UIImage>) -> ()))
To make this process faster, we will use background queues for image downloading and filtering. This way, the application UI is not blocked by the network request. When the image downloading finishes, filtering will be dispatched to the main queue.
There are several steps involved into filtering an image with this method:
- Create
URL
object fromurlString
parameter, so that image data can be fetched - Create background queue for executing network requests
- Fetch image binary data from
URL
object on background queue - Check if fetched data can be used to create
UIImage
object - Create
UIImage
from fetched data - Create and apply filter on the main queue
- Pass appropriate result via closure
A good implementation of this operation should cover all these steps and handle all errors that can occur:
- Url string parameter can have incorrect format
- Image fetching might fail because of networking issues
- Fetched data might not be an actual image
Example implementation
Let’s define our errors first. This example will use simple PhotoError
enum to define them. Of course, you can always use errors other than the ones you have defined.
enum PhotoError: Error {
// Invalid url string used
case invalidURL(String)
// Invalid data used
case invalidData
}
After the errors, the method body is defined:
func applyFilterToImage(at urlString: String, completion: @escaping ((Result<UIImage>) -> ())) {
// Check if `URL` object can be created from the URL string
guard let url = URL(string: urlString) else {
completion(.error(PhotoError.invalidURL(urlString)))
return
}
// Create background queue
let backgroundQueue = DispatchQueue.global(qos: .background)
// Dispatch to background queue
backgroundQueue.async {
do {
let data = try Data(contentsOf: url)
// Check if `UIImage` object can be constructed with data
guard let image = UIImage(data: data) else {
completion(.error(PhotoError.invalidData))
return
}
// Dispatch filtering to main queue
DispatchQueue.main.async {
// Crate sepia filter
let filter = CIFilter(name: "CISepiaTone")!
// Setup filter options
let inputImage = CIImage(image: image)
filter.setDefaults()
filter.setValue(inputImage, forKey: kCIInputImageKey) // Set input image
// Get filtered image
let filteredImage = UIImage(ciImage: filter.outputImage!)
// Return successful result
completion(.success(filteredImage))
}
} catch {
// Dispatch error completion to main queue
DispatchQueue.main.async { completion(.error(error)) }
}
}
}
As you can see, methods that can throw errors are encapsulated within a do { } catch { }
block. First, the URL
object is created. If urlString
is invalid, PhotoError.invalidURL
will be returned. This pattern of error checking follows the rest of the method. If every operation was successful, a successful result with the filtered image will be returned.
Let’s say that we want to use this method on a local photo named landscape.jpeg
. We create a imageURL
and then execute the applyFilterToImage
method. To check if image filtering was successful, we can use switch
statement. If the result is successful, its associated UIImage
object can be used as any other object.
let imageURL = Bundle.main.path(forResource: "landscape", ofType: "jpeg")!
applyFilterToImage(at: imageURL) { result in
switch result {
case let .success(image):
let someImageView = UIImageView()
someImageView.image = image
case let .error(error):
print(error)
}
}
Conclusion
Result-oriented programming is an important tool for any Swift developer. It can make writing and handling asynchronous code easier and more intuitive. The idea behind it is very simple and easy to grasp, even for beginners.
Frequently Asked Questions (FAQs) about Swift Closures
What is the main difference between Swift closures and functions?
Swift closures and functions are similar in many ways. Both can accept parameters and return values. However, the main difference lies in their context. Functions are named code blocks that can be called by their name anywhere in your code. On the other hand, closures are unnamed, self-contained blocks of code that can be passed around and used in your code. Closures can capture and store references to any constants and variables from the context in which they are defined. This is known as closing over those constants and variables, hence the name “closures”.
How can I use Swift closures in my code?
Swift closures can be used in several ways in your code. They can be assigned to a variable or constant, passed as an argument to a function, or returned from a function. Here’s an example of assigning a closure to a variable:let greet = { (name: String) in
print("Hello, \(name)!")
}
greet("John")
In this example, the greet
variable holds a closure that takes a String
parameter and does not return a value.
What are escaping closures in Swift?
An escaping closure is a closure that is passed as an argument to a function, but is called after the function completes. In other words, the closure escapes the function body. Escaping closures are marked with the @escaping
keyword in the function signature. Here’s an example:func someFunction(completion: @escaping () -> Void) {
DispatchQueue.main.async {
completion()
}
}
In this example, the completion
closure is called after the someFunction
function completes, hence it is an escaping closure.
What is the purpose of the @autoclosure
attribute in Swift?
The @autoclosure
attribute in Swift is used to delay the execution of a closure. When you mark a closure parameter with the @autoclosure
attribute, Swift automatically creates a closure from the expression you pass in. This allows you to delay the execution of the expression until you call the closure. Here’s an example:func logIfTrue(_ predicate: @autoclosure () -> Bool) {
if predicate() {
print("True")
}
}
logIfTrue(2 > 1)
In this example, the predicate
closure is not executed until it is called within the logIfTrue
function.
How can I capture values in a Swift closure?
Swift closures can capture and store references to any constants and variables from the context in which they are defined. This is known as closing over those constants and variables. Here’s an example:func makeIncrementer(incrementAmount: Int) -> () -> Int {
var total = 0
let incrementer: () -> Int = {
total += incrementAmount
return total
}
return incrementer
}
let incrementByTwo = makeIncrementer(incrementAmount: 2)
print(incrementByTwo()) // Prints "2"
print(incrementByTwo()) // Prints "4"
In this example, the incrementer
closure captures and stores a reference to the total
variable and the incrementAmount
constant from its surrounding context.
Said Sikira, 21, working as an iOS developer at iZettle, Stockholm. Loves playing and composing music on piano and guitar.