Kodeco Forums

Video Tutorial: Introducing Concurrency Part 6: NSOperation in Practice

Pull together all the concurrency knowledge you've learned so far in this series to improve the scrolling performance of a table view in a realistic app.


This is a companion discussion topic for the original entry at https://www.raywenderlich.com/4034-introducing-concurrency/lessons/7

Hi Sam, I got a tricky question, let’s say some of your cells share the same image,

you don’t want to create more than 1 network request for the same image

one way you be to check if there is not already a TiltShiftImageProvider instance in the set in tableView willDisplayCell and not create the instance but you would some mechanism to subscribe your cell to this completion handler

Example:

Cell 1 - image 1
Cell 2 - image 1
Cell 3 - image 1
Cell 4 - image 2
Cell 5 - image 3

when the operation of image 1 finish it should also update cell 2 & 3 since they display the same image

thanks

Hiya,

In this instance I think I would create a separate class which is responsible for all network operations. A cell would use this manager object to request an image, and the manager would operate some kind of cache.

This way, had the image already been downloaded then it could be returned immediately. If it hadn’t been downloaded you could check some kind of array of “in-progress requests” - and as you say, if there’s already a request for the specified image then you’d add another completion handler to the network operation, as you suggested.

Hope that helps - it’s a slightly challenging design problem, but using a shared network controller should allow you to achieve what you’re after.

sam

thanks Sam, I love the idea, having another abstraction layer up front to manage all network operations and caching.

However, there is one thing that is not clear to me, is this idea of subscribing or adding a second completion handler for a current one

either if this network manager used an NSOperation or an NSURLSessionDataTask

the network manager will be called from the table view with a closure. If there is already a network request for a particular image, the only way to wait until it finish and call back the closure is with KVO either on the operation status or URLSessionDataTask state

for the first request for an image is different cause I can used directly the NSOperation completion block to return or the NSURLSessionDataTask block

is KVO my only options?

thanks

If I were writing it, I think I would create a method on the network manager that takes the URL of the image request and a closure to call when it returns.

I would take the closure, and save it into some kind of dictionary, and then kick off the download task with a different closure. This closure would find all the requests that have been made for the image that it has downloaded (the closures you saved in the dictionary) and then call each of them in turn with the image you’ve just got hold of.

Does that make sense? If there’s a download operation in progress, just collect the closure and call it later.

sam

thanks Sam, I’ll have to give it a try, I did not know we could put closures in a dictionary, the important thing is that these closures need to captures the cell where they were called, so by calling them from the dictionary they will return to the table view at the appropriate indexPath …

in summary, your idea is to declare a closure, put it in a dictionary, trigger the download process into another closure and when that one finish, search for the first closure in the dictionary and call it, that will return the initial closure of the network manager to the tableView cell at the appropriate indexPath

alex

Pretty much yeah…

I think the dictionary would be URL to an array of closures. So when an image arrived, you could look up a list of closures to call using the URL.

You need to ensure that the table cell exists when you perform the update with the downloaded image - if the user is scrolling then it’s quite possible that the image you’ve just downloaded is no longer needed.

Hope that makes some sense - good luck! :rainbow:

sam

Hi Sam. Got a quick question. In the ImageFilterOutputOperation’s main function we call the completion handler with the image as filterInput. Correct me if i am wrong on my understanding of how we approach this
1)Until we reach the TiltShiftOperation dependency the only ImageFilterDataProvider object that exists in the dependencies array is just ImageDecompressionOperation. Therefore
2.) When TilitShiftOperation is run using the main function the filterInput is the image provided by ImageDecompressionOperation’s ImageFilterDataProvider.image isn’t it?
3.) So TilitShiftOperation uses the above image as filterInput and produces a filterOutput. Correct?
4.) So when we reach ImageFilterOutputOperation’s completion handler in the main func how come the image that we pass async is filterInput and not filterOuput. I tried changing the argument to filterOuput but when I ran the code the spinners keep on spinning but never load the images.

How is the filterInput now containing the filteredImage is my question

Hi @josephraj

I can see why this might be a little confusing - in the dependency chain, one filter’s output is another filter’s input.

Anything that subclasses ImageFilterOperation will search through the dependency tree to find an ImageFilterDataProvider, and returns its image property as the filter’s inputImage property. Since an image filter takes an image as an input, processes it, and then outputs another image, it conforms to the ImageFilterDataProvider protocol. It’s this that allows them to be chained together.

This architecture means that an ImageFilterOperation will search its dependency array for something that can provide it an input image, and will also vend its output, so that the next filter in the chain can use it as its input.

ImageFilterOutputOperation is a subclass of ImageFilterOperation, so its filterInput property will necessarily be the image property of the most recent ImageFilterDataProvider in the dependency chain. Since this is actually a filter (tilt-shift), then it’s actually the output of that filter.

I hope that clears it up a little for you - although it seems quite complex, it’s not that bad once you realise that one filter’s output is another’s input.

sam

Hey samdavies,

Great Vidoes.
I have a quick questions. Suppose I have a list of operations in a queue and I want to get notified when all the operations on the queue are done.

Some Solutions I came across :


  • set waitUntilFinished to true when calling operationQueue.addOperations(ops: [Operation], waitUntilFinished: Bool) which blocks the current thread until all operations are done(don’t want to do that) .

  • use operationQueue.addObserver(observer: self, forKeyPath: "operations", options: nil, context: nil) to observe the operations array and check if it has zero elements. (I would prefer not to use this unless I have to)

  • I can also set the maxConcurrentOperationCount to one and pass in a completion block to the last operation in the array . (don’t want to do that)

I was wondering if there is a better way of doing it.

Thanks

Hi @namuthan

A couple of other options for you:

  • waitUntilAllOperationsAreFinished() is a method on OperationQueue that blocks the current thread until all operations are completed. It’s basically the equivalent of setting waitUntilFinished to true on the addOperations(_:, waitUntilFinished:) method.
  • You could add a “completion” Operation and make it dependent on all of your other operations (using addDependency(_:)). Add this to the same operation queue and it won’t be executed until all operations it is dependent on complete.

I prefer the latter of these two options - it doesn’t tell you that the queue is finished, but rather all the operations you care about, which is semantically more satisfying.

Hope that helps.

sam

1 Like