2

I'm developing photo editor app, but I have a problem with render filtered images in UICollectionView. I'm using Operation and OperationQueue. When I start scrolling the collectionView, filtered images are being updated. How can I fix it?

ImageFiltration:

class ImageFiltration: Operation {
private let image: CIImage
private let filterName: String!

var imageWithFilter: CIImage?

init(image: CIImage, filterName: String) {
    self.image = image
    self.filterName = filterName
}

override func main() {
    if self.isCancelled {
        return
    }

    if let name = filterName {
        let filter = CIFilter(name: name, withInputParameters: [kCIInputImageKey: image])
        imageWithFilter = filter?.outputImage!
    }
} }

class PendingOperation {
lazy var filtrationInProgress = [IndexPath: Operation]()
lazy var filtrationQueue: OperationQueue = {
    var queue = OperationQueue()
    queue.name = "Filtration Operation"
    return queue
}() }

UIScrollViewDelegate:

extension PhotoEditorViewController: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    presenter.suspendAllOperations()
}

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if !decelerate {
        presenter.loadImagesForOnScreenCells(collectionView: filtersToolsView.collectionView) { (indexPath) in
            self.filtersToolsView.collectionView.reloadItems(at: [indexPath])
        }
        presenter.resumeAllOperations()
    }
}

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    presenter.loadImagesForOnScreenCells(collectionView: filtersToolsView.collectionView) { (indexPath) in
        self.filtersToolsView.collectionView.reloadItems(at: [indexPath])
    }
    presenter.resumeAllOperations()
} }

UICollectionView Data Source:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell: PhotoEditorFilterCollectionViewCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)

    cell.layer.shouldRasterize = true
    cell.layer.rasterizationScale = UIScreen.main.scale
    cell.filteredImage.contentMode = .scaleAspectFill

    presenter.startFiltration(indexPath: indexPath) { (image, filterName) in
        cell.filteredImage.inputImage = image
        cell.filterNameLabel.text = filterName
    }

    return cell
}

Implementation start filtration method:

func startFiltration(indexPath: IndexPath, completion: @escaping (CIImage?, String) -> ()) {
    if let _ = pendingOperations.filtrationInProgress[indexPath] {
        return
    }

    let filteredImage = ImageFiltration(image: model.image,
                                        filterName: model.image.filters[indexPath.row].filterName)

    filteredImage.completionBlock = {
        if filteredImage.isCancelled {
            return
        }

        DispatchQueue.main.async {
            self.pendingOperations.filtrationInProgress.removeValue(forKey: indexPath)
            completion(filteredImage.imageWithFilter, self.model.image.filters[indexPath.row].filterDisplayName)
        }
    }

    pendingOperations.filtrationInProgress[indexPath] = filteredImage
    pendingOperations.filtrationQueue.addOperation(filteredImage)
}

Operation methods:

func suspendAllOperations() {
    pendingOperations.filtrationQueue.isSuspended = true
}

func resumeAllOperations() {
    pendingOperations.filtrationQueue.isSuspended = false
}

func loadImagesForOnScreenCells(collectionView: UICollectionView,
                                completion: @escaping (IndexPath) -> ()) {
    let pathsArray = collectionView.indexPathsForVisibleItems

    let allPendingOperations = Set(Array(pendingOperations.filtrationInProgress.keys))
    let visiblePaths = Set(pathsArray as [IndexPath])
    var toBeCancelled = allPendingOperations

    toBeCancelled.subtract(visiblePaths)

    var toBeStarted = visiblePaths

    toBeStarted.subtract(allPendingOperations)

    for indexPath in toBeCancelled {
        if let pendingFiltration = pendingOperations.filtrationInProgress[indexPath] {
            pendingFiltration.cancel()
        }

        pendingOperations.filtrationInProgress.removeValue(forKey: indexPath)
    }

    for indexPath in toBeStarted {
        let indexPath = indexPath as IndexPath
        completion(indexPath)
    }
}

I'm rendering a picture in GLKView.

Video

nik3212
  • 392
  • 1
  • 6
  • 18
  • 2
    Hi there. You need to remember that any user initiated operation will be given the highest priority in the run time, so when the collection view is scrolled your image processing code will pause. I think you need to do the image processing in the background with a callback to update the view once it has finished. If you need help with how to code that let me know. – Marcus Jan 20 '18 at 19:10
  • Unrelated to your question but making filterName an enum prevents invalid names – Hapeki Jan 22 '18 at 13:24

1 Answers1

1

On scrollViewDidScroll cancel all of your requests. And in end scrolling get visible cells and start your operation queue. This way it will only use your visible cell memory and UI will not stuck. See this tutorial it's same as your requirement - https://www.raywenderlich.com/76341/use-nsoperation-nsoperationqueue-swift

Amrit Trivedi
  • 1,240
  • 1
  • 9
  • 24