Contents

4 Ways To Pass Data Between Operations With Swift


Do you appreciate the power of Operations and would you like to pass the data between them to make amazing chains? Let’s read 4 ways to achieve your dreams.

Introduction

In this article, we are going to see some approaches to pass the data between two Operations in Swift. To avoid losing the focus on this topic, I will not explain what is and how works an Operation. For this reason, you need a basic understanding of Operation to understand the next sections of the article.

I may write another article to explain the Operation if I see that you would be interested on it.

Happy Reading!

Getting Started

Before diving into these approaches, we need a scenario for our examples. I guess all of us made an application where we had to fetch the data from an API request and then parse the data received. For this reason, I think it’s quite familiar if we use a scenario where we have two Operations: one to fetch and one to parse the data.

We can start creating our two Operation classes:

FetchOperation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
final class FetchOperation: Operation {

    // 1
    private(set) var dataFetched: Data?

    override func main() {
        // 2
        self.dataFetched = // data received from HTTP request
    }
}
  1. The data fetched to send to ParseOperation.
  2. Saves the data received from an HTTP request.

For the sake of explanation, I skipped a real implementation since it would need an asynchronous operation. If you want to learn how to use an asynchronous operation, you can have a look at my gist.

ParseOperation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
final class ParseOperation: Operation {

    // 1
    var dataFetched: Data?

    // 2
    private(set) var jsonParsed: [String: Any]?

    override func main() {
        // 3
        guard let dataFetched = dataFetched else { return }
        jsonParsed = try! JSONSerialization.jsonObject(with: dataFetched, options: []) as? [String: Any]
        print(jsonParsed)
    }
}
  1. The data received from FetchOperation.
  2. The dictionary created from the parsing of dataFetched.
  3. Checks if the data exists and then creates a dictionary from the data fetched.

For the sake of explanation, I kept both Operation implementations as plain as possible without caring of the lifecycle.

The last step is creating a handler class which will manage these operations with an OperationQueue:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
final class Handler {

    // 1
    private let queue: OperationQueue = OperationQueue()

    func start() {
        // 2
        let fetch = FetchOperation()
        let parse = ParseOperation()
        parse.addDependency(fetch)

        // 3
        queue.addOperations([fetch, parse], waitUntilFinished: true)
    }
}
  1. OperationQueue to run our Operations.
  2. Prepares our Operations setting the dependencies.
  3. Adds the Operations in the queue blocking the queue thread until it’s finished.

With these 3 classes, we are ready to start looking at the approaches to pass dataFetched from FetchOperation to ParseOperation.

Approaches

Internal dependency reference

The object Operation provides an array of its dependencies with the following property:

1
var dependencies: [Operation] { get }

Thanks to this information, in ParseOperation we can have access to its dependency FetchOperation:

1
let fetchOperation = dependencies.first as? FetchOperation

At this point, we can refactor the method main of ParseOperation to read dataFetched directly from its dependency:

1
2
3
4
5
6
7
8
override func main() {
    guard let fetchOperation = dependencies.first as? FetchOperation else { return }
    self.dataFetched = fetchOperation.dataFetched

    guard let dataFetched = dataFetched else { return }
    jsonParsed = try! JSONSerialization.jsonObject(with: dataFetched, options: []) as? [String: Any]
    print(jsonParsed)
}

This approach is the easiest since we don’t need any external helpers to inject dataFetched. To be honest, I don’t like this approach. I would prefer injecting the data from outside because ParseOperation wouldn’t have the responsibility to decide where to get the data.

Reference Wrapper

For this approach, we have to create a new class which will wrap fetchedData:

1
2
3
final class DataWrapper {
    var dataFetched: Data?
}

Then, we can inject this new wrapper in both operations. FetchOperation will use this wrapper to set the property dataFetched, whereas ParseOperation will read the value of dataFetched—previously set in FetchOperation.

We can change our FetchOperation to inject this wrapper and set its property once we receive the HTTP response:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
final class FetchOperation: Operation {

    private let dataWrapper: DataWrapper

    // 1
    init(dataWrapper: DataWrapper) {
        self.dataWrapper = dataWrapper
    }

    override func main() {
        // 2
        dataWrapper.dataFetched = // data received from HTTP request
    }
}
  1. Injects DataWrapper and keeps an internal reference to use in main.
  2. Sets the wrapper property to be used in ParseOperation.

Then, we can change ParseOperation to read the wrapper property:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
final class ParseOperation: Operation {

    private(set) var jsonParsed: [String: Any]?

    private let dataWrapper: DataWrapper

    // 1
    init(dataWrapper: DataWrapper) {
        self.dataWrapper = dataWrapper
    }

    override func main() {
        // 2
        guard let dataFetched = dataWrapper.dataFetched else { return }
        jsonParsed = try! JSONSerialization.jsonObject(with: dataFetched, options: []) as? [String: Any]
        print(jsonParsed)
    }
}
  1. Injects DataWrapper and keep an internal reference to use in main.
  2. Reads the wrapper property to parse it.

Finally, we can change the method start of Handler to use the new wrapper object:

1
2
3
4
5
6
7
8
9
func start() {
    let dataWrapper = DataWrapper()

    let fetch = FetchOperation(dataWrapper: dataWrapper)
    let parse = ParseOperation(dataWrapper: dataWrapper)
    parse.addDependency(fetch)

    queue.addOperations([fetch, parse], waitUntilFinished: true)
}

To be honest, I don’t like also this approach. We cannot inject just the data but we must inject this wrapper—which may not have the data ready when we use it in ParseOperation.

Keep reading to learn better approaches.

Completion block

The object Operation provides a completion closure which is called once the Operation completes its task:

1
var completionBlock: (() -> Swift.Void)?

We can take advantage of this completion to pass the values between the two Operations:

1
2
3
fetch.completionBlock = { [unowned parse, unowned fetch] in
    parse.dataFetched = fetch.dataFetched
}

Remember to use unowned for both operation objects otherwise you would create a retain cycle.

At this point, we can refactor the method start of Handler like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func start() {
    queue.maxConcurrentOperationCount = 1

    let fetch = FetchOperation()
    let parse = ParseOperation()
    parse.addDependency(fetch)

    fetch.completionBlock = { [unowned parse, unowned fetch] in
        parse.dataFetched = fetch.dataFetched
    }

    queue.addOperations([fetch, parse], waitUntilFinished: true)
}

If you don’t set maxConcurrentOperationCount of OperationQueue to 1, parse would start without waiting the completion block of FetchOperation. It means that we would inject the data too late when the operation is already started. Instead, we must inject it before running ParseOperation.

I definitely prefer this approach rather than both Internal dependency reference and Reference Wrapper since we can inject dataFetched from outside.

Update:

As the user HoNooD pointed out, this approach is not reliable. Even if we set maxConcurrentOperationCount of OperationQueue to 1, ParseOperation may start without waiting the completion block of FetchOperation. It’s quite inconsistent. For this reason, do not use this approach.

Adapter operation

This approach is very similar to Completion block. Instead of using the completion block, we add a third operation which is called Adapter.

This new Operation has just a plain block where we can inject the data fetched inside ParseOperation like in Completion block:

1
2
3
let adapter = BlockOperation(block: { [unowned parse, unowned fetch] in
    parse.dataFetched = fetch.dataFetched
})

At this point, we can refactor the method start of Handler like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func start() {
    let fetch = FetchOperation()
    let parse = ParseOperation()

    // 1
    let adapter = BlockOperation() { [unowned parse, unowned fetch] in
        parse.dataFetched = fetch.dataFetched
    }

    // 2
    adapter.addDependency(fetch)
    parse.addDependency(adapter)

    // 3
    queue.addOperations([fetch, parse, adapter], waitUntilFinished: true)
}
  1. Sets new adapter operation with a trailing closure.
  2. The dependencies have been changed:
    • adapter needs fetch as dependency to start when we fetched the data.
    • parse needs adapter as dependency to inject the data fetched.
    • parse no longer needs fetch as dependency since adapter is in the middle.
  3. Adds adapter in the queue.

Thanks to this adapter operation, we don’t need to care about maxConcurrentOperationCount of OperationQueue like in Completion block. We can leave its default value—OperationQueue.defaultMaxConcurrentOperationCount.

Conclusion

Personally, my favorite approach is Adapter operation since it provides a clean way to inject the data. You may argue that Completion block provides a clean solution as well without using another Operation in the middle. I agree with it, but I don’t like that we must set maxConcurrentOperationCount to 1 to avoid unexpected behaviours.

If you have better approaches, feel free to write them in the comments. Thank you!