1

I am using Rob's implementation of AsynchronousOperation

Following is my AsynchronousOperation subclass

    class AsynOperation : Operation {

        // Same implementation as mentioned in Rob's answer

    }

I'm trying to make a thumbnail for images/videos. I've wrapped the thumbnail creation functions inside an NSOperation

class AssetDocFetchOperation : AsynOperation, AssetDocGrabberProtocol {
    var cacheKey: URL?
    var url : String
    var image  : UIImage?

    init(url : String, cacheKey : URL?) {
        self.url = url
        self.cacheKey = cacheKey
    }
}

Image Generator

class AssetImageGrabber : AssetDocFetchOperation{

    let client = NetworkClient()
    override init(url : String,cacheKey : URL?) {
        super.init(url: url,cacheKey : cacheKey)
        // name = cacheKey?.absoluteString
    }


    override func main() {
        guard let docURL =  URL(string: self.url) else {
            self.finish()
            return
        }

        if let cKey = cacheKey ,let imageData = MemoryCache.shareInstance.object(forKey: cKey.absoluteString)  {
            self.image = UIImage(data: imageData)
            self.finish()
            return
        }

        client.shouldCache = false
        let service = AssetDocFetchService(url:docURL )
        client.request(customHostService: service) {[weak self] (result) in
            guard let this = self else {return}

            switch result {
            case .success(let data) :
                if let imageData = data, let i = UIImage(data: imageData){
                    if let cKey = this.cacheKey {
                        MemoryCache.shareInstance.set(object: imageData , forKey: cKey.absoluteString, cost: 1)
                    }
                    this.image = i
                    this.finish()
                }
            case .failure(let e):
                print(e)
                this.image = nil
                this.finish()
            }

        }
    }

    override func cancel() {
        super.cancel()    
        client.cancel()
    }
}

Video thumbnail creator

class AssetVideoThumbnailMaker : AssetDocFetchOperation  {


    private var  imageGenerator : AVAssetImageGenerator?

    override init(url : String,cacheKey : URL?) {
        super.init(url: url,cacheKey : cacheKey)
        //name = cacheKey?.absoluteString
    }



    override func main() {
        guard let docURL =  URL(string: self.url) else {
            self.finish()
            return
        }
        if let cKey = cacheKey ,let imageData = MemoryCache.shareInstance.object(forKey: cKey.absoluteString)  {
            self.image = UIImage(data: imageData)
            self.finish()
            return
        }
        generateThumbnail(url: docURL) {[weak self] (image) in
            self?.image = image
            self?.finish()
        }


    }
    override func cancel() {
        super.cancel()
        imageGenerator?.cancelAllCGImageGeneration()
    }


    private func generateThumbnail(url : URL ,completion: @escaping (UIImage?) -> Void) {
        let asset = AVAsset(url: url)
        imageGenerator = AVAssetImageGenerator(asset: asset)
        let time = CMTime(seconds: 1, preferredTimescale: 60)
        let times = [NSValue(time: time)]
        imageGenerator?.generateCGImagesAsynchronously(forTimes: times, completionHandler: { [weak self] _, image, _, _, _ in
            if let image = image {
                let uiImage = UIImage(cgImage: image)
                if let cKey = self?.cacheKey , let data = uiImage.pngData() {
                    MemoryCache.shareInstance.set(object: data , forKey: cKey.absoluteString, cost: 1)
                }
                completion(uiImage)
            } else {
                completion(nil)
            }
        })

    }
}

I'll create NSOperation from cellForItemAt method

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let file : AssetFile = // set the file here
        .... 
        let grabOperation  = taskMaker(fromDocFile : file)
        cell.assetFile = grabOperation
        if grabOperation.state.value == .initial   {
           start(operation: grabOperation)
        }
        return cell
    }



func taskMaker(fromDocFile file : AssetFile)->AssetDocFetchOperation{
        switch file.fileType {
        case .image:
            return AssetImageGrabber(url : file.path ?? "",cacheKey: file.cacheKeyURL)
        case .video :
            return AssetVideoThumbnailMaker(url : file.path ?? "",cacheKey: file.cacheKeyURL)
        }
    }

I'll add them to the operation queue as follows

lazy var docsOperations : OperationQueue = {
        let queue = OperationQueue()
        queue.name = "assetVideoOperations"
        queue.maxConcurrentOperationCount = 3
        return queue
    }()
lazy var docsOperationInProgress : [AssetGrabOperation] = []

    func start(operation : AssetGrabOperation){
        if !docsOperationInProgress.contains(operation) && !operation.task.isFinished && !operation.task.isExecuting {

                docsOperationInProgress.append(operation)
                docsOperations.addOperation(operation.task)
        }
    }

For failed/timed out request, I've retry method

func reloadDocument(atIndex: IndexPath?) {
        if let index = atIndex {
            let tile : AssetFile = // get file here 
            let grabOperation  = // get current opration 
            remove(operation: grabOperation)
            reloadFile(atIndexPath: index) // i'll recreate the operation with taskMaker(fromDocFile : file)
            documentList.reloadItems(at: [index])
        }
    }

The remove operation method

 func remove(operation :AssetGrabOperation ) {
    operation.task.cancel()
    docsOperationInProgress = docsOperationInProgress.filter({ return $0 == operation ? false : true })
 }

The problem is, if I scroll back and forth in my UICollectionView the app crashes throwing the following error. (Sometimes when I call the reloadDocument as well)

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSOperationQueue addOperation:]: operation is already enqueued on a queue'

I've also found a solutions/workaround by checking the names of the operation

        let alreadyOn =  docsOperations.operations.filter({
           return  $0.name == operation.task.name
        })
        // i'll assign the cacheURL to name while the init of opration 
        if alreadyOn.count ==  0 {
            docsOperationInProgress.append(operation)
            docsOperations.addOperation(operation.task)
        }

I'm not sure whether this approch is good or not. What am i doing wrong?

Sayooj
  • 375
  • 3
  • 13

1 Answers1

0

After a long time to fix this issue, I did make an extension for Operation to remove dependencies before starting the new Operation, This could be a workaround solution

extension Operation { 
     public func removeAllDependencies() {
        dependencies.forEach {
              $0.removeAllDependencies()
              removeDependency($0) } 
    }
}
Marwan Alqadi
  • 795
  • 8
  • 14