Improve Your Swift Closures with Result

Share this article

Improve Your Swift Closures with Result

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.

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"
})

https://i.imgur.com/Gw6IFHU.png

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:

  1. Create URL object from urlString parameter, so that image data can be fetched
  2. Create background queue for executing network requests
  3. Fetch image binary data from URL object on background queue
  4. Check if fetched data can be used to create UIImage object
  5. Create UIImage from fetched data
  6. Create and apply filter on the main queue
  7. 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 applyFilterToImagemethod. 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 SikiraSaid Sikira
View Author

Said Sikira, 21, working as an iOS developer at iZettle, Stockholm. Loves playing and composing music on piano and guitar.

closuresiosmacosswift
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week