Freezes when scrolling UICollectionView

2.3k Views Asked by At

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

1

There are 1 best solutions below

1
Amrit Trivedi On

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