Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

4 Ways to Pass Data Between Operations With Swift

DZone's Guide to

4 Ways to Pass Data Between Operations With Swift

Consider a common scenario where you have two operations — one to fetch data and one to parse it — and learn some approaches for passing the data between them in Swift.

· Big Data Zone
Free Resource

Intelligently automate your Big Data operations to lower your costs, make your team more productive, scale more efficiently, and lower the risk of failure. Learn how >>

In this article, we are going to see some approaches for passing the data between two operations in Swift. To avoid losing the focus on this topic, I will not explain what an operation is and how it works — you need a basic understanding of operations to understand the rest of the article.

I may write another article to explain the Operation class if I see that you would be interested in it!

Before diving in, we need a scenario for our examples. We've all 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 a scenario where we have two operations — one to fetch the data and one to parse the data — will be quite familiar.

We can start creating our two Operation classes:

FetchOperation:

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 asynchronous operations, you can have a look at my gist.

ParseOperation:

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 implementations as plain as possible without caring about the lifecycle.

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

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 three classes, we are ready to start looking at the approaches to pass dataFetched from FetchOperation to ParseOperation.

Approaches

The approaches we'll look at are internal dependency reference, reference wrapper, completion block, and adapter operation.

Internal Dependency Reference

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

var dependencies: [Operation] { get }

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

let fetchOperation = dependencies.first as? FetchOperation

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

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 the outside because ParseOperation doesn’t have the responsibility of deciding where to get the data.

Reference Wrapper

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

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:

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:

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:

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 that is called once the operation completes its task:

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

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

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

Remember to use unowned for both operation objects. Otherwise, you will create a retain cycle.

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

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 will start without waiting for the completion block ofFetchOperation. This 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 the internal dependency reference approach and reference wrapper approach since we can inject dataFetched from outside.

Adapter Operation

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

This new operation has a plain block where we can inject the data fetched inside ParseOperation like in the completion block approach:

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:

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 a dependency to start when we fetch the data.
    • parse needs adapter as a dependency to inject the data fetched.
    • parse no longer needs fetch as a 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 the maxConcurrentOperationCount of OperationQueue like in the completion block approach. 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 — but I don’t like that we must set maxConcurrentOperationCount to 1 to avoid unexpected behaviors.

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

Find the perfect platform for a scalable self-service model to manage Big Data workloads in the Cloud. Download the free O'Reilly eBook to learn more.

Topics:
big data ,swift ,tutorial ,passing data ,parse ,fetch

Published at DZone with permission of Marco Santarossa, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}