In the last video, we defined an operation to apply a filter to a given image.
The next step is to define an operation queue that we can schedule these
operations on.
Again, this is content that we've covered before, so
I won't go into too much detail.
If you want a more thorough discussion, check the links in the teacher's notes,
Navigate to the PhotoFilterController.swift file.
We're going to add some code in here and not worry about creating a new file,
because we're going to implement a different approach later anyway.
Let's add two stored properties to this class,
first is the operation queue we'll add this right after the context.
So, let filtertrationQueue = OperationQueue is an instance.
Second, we'll add a set to keep track of operations in progress.
This way we'll make sure we don't add two operations for the same filter process.
We won't record the actual operation for this,
since the operation is associated with a cell, thereby an index path,
we're just going to keep track of the index path.
Now, in a more realistic app, if we were to keep track of operations,
we do want something like a dictionary instead of a set.
Because we do want a reference to the operation, so we can cancel it or
pause it if it goes off screen.
So we'll say, var filtrationsInProgress,
and this is a set that contains IndexPath.
At the bottom of the file,
we have an extension of the class where we added our data source methods, right here.
Let's define a method in here, in this extension to start an operation and
add it to the queue.
So, we'll call this startFiltrationOperationForImage
takes an image of type FiltrationImage,
withFilter of type CIFilter,
at a given indexPath of type IndexPath.
All right, so this method takes three arguments,
the image to filter which is always the same image.
But because we need to track it's filter state we're going to wrap that in
an instance of filtration image.
Then it's taking the filter that we want to apply, and
finally the index path of the cell that we're creating this operation for.
This last part is important because we want to keep track of which cells we've
already defined operations for.
We don't want to form any filtration operations twice because as we've seen,
it's quite expensive.
Inside the body of this method, the first thing we'll do is check if we've already
created an operation for the current IndexPath.
We'll say, if filtrationsInProgress.contains this
indexPath, then we're just going to return that method.
Now, the reason we're doing this is because we're going to set this code up,
the startFiltrationOperation,
we're going to call this inside the data source methods.
So every time you scroll in your collection view and
cells go off screen and reappear back screen, this method,
the contents of this method is going to be run again, we know that.
And when it does, it's going to call this method and if we don't check for this,
it's going to create these duplicate operations, and we don't want to do that.
So if we already have an operation we're going to return without doing anything,
if we don't have an operation, let's create one.
So, let operation = ImageFiltrationOperation
takes an image of type FiltrationImage and a filter.
When the operation is complete, let's bump this up the screen, there we go.
When the operation is complete, we're going to remove the indexPath from our
set, to indicate we're not keeping track of it anymore, and
reload the relevant collection view cell.
Any logic we want executed after the operation completes is added to
the completion block property, which accepts a closure.
So, here we'll say operation.completionBlock, and
we'll assign a closure.
The signature of this closure is void to void, so
we can't return anything out of it.
Now, with operations we always have to check if it's been cancelled, so
that we don't unnecessarily do additional work.
So if operation is cancelled, then return,
don't even execute anything inside the completion block.
If the operation completed successfully, we know that the image assigned to
the image property of the filtration image associated with this operation.
Now has a filter on it, and we can reload the relevant collection view cells.
Since this is a UI operation, and this operations are happening on back graphic
or running on a back graphic, we need to jump back on to the main thread.
We'll also need to remove the IndexPath from the set to indicate that this cell
no longer has an active operation associated with it.
So I'll say DispatchQueue.main, cuz we want the main thread and
we want to execute this asynchronously.
So in this closure, we're going to say
self.filtrationsInProgress.remove the current indexPath,
and self.filtersCollectionView.reloadItems at indexPath.
And that's all we'll do when the operation completes.
The last step is to actually add the operation to the queue and
track it as active.
So after this completion lock, down here we'll say
filtrationsInProgress.insert the current indexPath and
filtrationQueue.addOperation, the operation we just defined.
Okay, so we now have the code to start and conduct the operation, but
that's not all we need.
We need to call this method somewhere, and that somewhere is in the data source
method, right over here, cellForItemAt indexPath.
So, we're going to get rid of all this code between the two lines where we
created the cell and return it, and we'll provide a completely new implementation.
When the collection view sets up each cell,
instead of directly assigning a filtered image,
now we're going to create an operation and add it to the queue.
Once the operation is over, we'll assign the relevant image, but
each operation requires a filter and a filtration image.
So before we can start writing any code here, let's go up to the top and
add two new stored properties that provide these initial values.
So we'll do it down here, we'll say lazy var filters, so this is for
the array of filters, and this is easy because we've already defined it.
So this is going to be an array of CIFilter and well actually,
we don't even need to make this a lazy store property.
We'll just say let filters = PhotoFilter.default,
oops, if I can spell PhotoFilter.defaultFilters, there we go.
The filtration images, those are easy to define,
we just have to think about how we'll do it.
Since we're using the same image for all instances of the filtration image,
we just need to have the same number of filtration images as we have filters.
And so to achieve that, we can map over the array of filters and
return an instance of filtration image for each.
So, lazy var filtrationImages, is going to be
type FiltrationImage on array of filtration image,
and in here we first need that photo, so we'll say,
guard let image = self.photo else return an empty array.
This is the same thing we're doing up here, so nothing new.
Then we're going to return self.filters.map, and in here we don't
actually care about the filters, I'm going to discard it and say, in.
And we'll return an instance FiltrationImage, and
we'll pass the same image through every time.
Now the reason we're doing this,
is just to have the same number of filtration images as we have filters.
Okay, now that we have these two arrays, let's delete this filteredImages one,
we don't need that anymore.
And now we can go back down and clean up our data source methods.
Which is right here.
So in numberOfItemsInSection, instead of filteredImages.count,
we'll say filtrationImages.count.
And then inside cell for item we'll grab a filtrationImage,
let's say, filtrationImages[indexPath.row].
We're going to check the filter state on this instance to see if it's been filtered
or not.
If it has, then we'll assign the image to the cells image view.
So we'll say, if, and
there's that it's pretty much guaranteed that it's not going to be filtered.
So we'll say, if filtrationImage,
at least on that first path, .filterState = filtered,
then cell.imageView.image = filtrationImages,
no filtrationImage.image, right.
So let's assume we've completed our operations, and
we've modified the image on the underlying filtration image to have a filter on it.
Now this array has a bunch of filtered images, and we can check, oops typo here.
And now this will return true, and so we'll go ahead and assign an image.
But on that first pass it's not going to be filtered, so
we're not going to assign anything to the cell.image.View, and instead we'll say,
this is nil, so it doesn't have any image.
So not the best approach but good enough for this course,
realistically you should have some place holder image over here.
Okay, now we need to create the operation, to do that we need a filter.
So, let filter = filters[indexPath.row] since we only want to
create an operation if the image does not have a filter applied,
we'll query the filtered state value again.
So we'll say, switch filtrationImage.filterState, and
in case of noFilter,
we're going to spin up an operation using the method we wrote earlier.
So, startFiltrationOperation for
filtrationImage withFilter at indexPath, for
the filtered state, then we'll just break out of the switch.
Now, you can actually combine these two lines of code because these are both
cases where we're coding the filtered state on the filtrationImage.
But this actually makes it easier it to understand what's going on.
Now, if you run the app now, and again I won't and
that's because you'll see a tiny performance gain here.
But unfortunately it's still going to be pretty slow,
let's go to the next video to find out why.