Swift: Retain cycle with NSOperation

In my app I use an image loader class to load images from the web for a collection view. The class keeps track of the download operations and cancels them when the cells for the images are no longer visible in the collection view. This implementation is based on the raywenderlich tutorial for NSOperation: http://www.raywenderlich.com/76341/use-nsoperationnsoperationqueueswift.

I use NSOperation for downloading an image from the web. I noticed with Instruments that none of the NSoperations is released. This creates an increase of the used memory for each image that is downloaded. In the completion block I references ‘self’. So I figured out that I created a retain cycle.

  • How do I set up a simple delegate to communicate between two view controllers?
  • How to get path to a subfolder in main bundle?
  • Swift.Extensions var and func. Which one is better?
  • Code=-1001 “The request timed out.”
  • Make the application restart by itself on a jailbroken device
  • How to save local data in a Swift app?
  • I read a lot of examples on internet. I understand that I can use capture lists with ‘weak self’ or ‘unowned self’. I tried this for the completion block, but still the operations are not released.

    My code for the image loader class is as follows:

    import Foundation
    import UIKit
    
    class ImageLoader {
        lazy var downloadsInProgress = [NSIndexPath:NSOperation]()  
        lazy var downloadQueue:NSOperationQueue = {
            var queue = NSOperationQueue()
            queue.name = "Image Download queue"
            return queue
        }()
    
        let cache = NSCache()       // contains NSData objects for images
    
        init() {
            // Max. cache size is 10% of available physical memory (in MB's)
            cache.totalCostLimit = 200 * 1024 * 1024    // TODO: change to 10%
        }
    
        /**
         * Download image based on url for given indexpath. 
         * The download is only started if the indexpath is still present in the downloadsInProgress array
         */
    
        func startDownloadForUrl(url: String, indexPath: NSIndexPath, completion: (imageData: NSData?) -> Void) {
            // check if download request is already present
            if downloadsInProgress[indexPath] != nil {
                return
            }
    
            // check cache
            if let imageData = self.cache.objectForKey(url) as? NSData {
                NSOperationQueue.mainQueue().addOperationWithBlock() {
                    //remove indexpath from progress queue
                    self.downloadsInProgress.removeValueForKey(indexPath)
                    completion(imageData: imageData)
                }
                return
            }
    
            // prepare the download
            let downloader = ImageDownloader(url: url)
    
            downloader.completionBlock = {
                [unowned self] in
    
                if downloader.cancelled {
                    return
                }
    
                // image is retrieved from web
                NSOperationQueue.mainQueue().addOperationWithBlock() {
                    [unowned self] in
    
                    //remove indexpath from progress queue
                    self.downloadsInProgress.removeValueForKey(indexPath)
    
                    // add image to cache
                    if downloader.imageData != nil {
                        self.cache.setObject(downloader.imageData!, forKey: url, cost: downloader.imageData!.length)
                    }
                    completion(imageData: downloader.imageData)
                }
            }
    
            // add downloader to operations in progress and start the operation
        NSOperationQueue.mainQueue().addOperationWithBlock() {
                [unowned self] in
    
                self.downloadsInProgress[indexPath] = downloader
                self.downloadQueue.addOperation(downloader)
            }
        } 
    
    
        /**
         * Suspends queue for downloading images
         */
    
        func suspendAllOperations() {
            downloadQueue.suspended = true
        }
    
    
        /**
         * Resumes queue for downloading images
         */
    
        func resumeAllOperations() {
            downloadQueue.suspended = false
        }
    
    
        /**
         * Cancels downloads for NOT visible indexpaths. The parameter specifies an array of visible indexpaths!
         */
    
        func cancelDownloads(visibleIndexPaths: [NSIndexPath]) {
            let allPendingOperations = Set(downloadsInProgress.keys)
            let visiblePaths = Set(visibleIndexPaths)
    
            // cancel all pending operations for indexpaths that are not visible
            var toBeCancelled = allPendingOperations
            toBeCancelled.subtractInPlace(visiblePaths)
    
            for indexPath in toBeCancelled {
                if let pendingDownloadOperation = downloadsInProgress[indexPath] {
                    pendingDownloadOperation.cancel()
                }
    
                downloadsInProgress.removeValueForKey(indexPath)
            }
        }
    }
    
    
    class ImageDownloader: NSOperation {
        var url: String
        var imageData: NSData?
    
        init(url: String) {
            self.url = url
        }
    
        override func main() {
            if self.cancelled {
                return
            }
    
            if let imageUrl = NSURL(string: url) {
                // retrieve data from web
                setNetworkActivityIndicatorVisible(true)
                imageData = NSData(contentsOfURL: imageUrl)
                setNetworkActivityIndicatorVisible(false)
    
                if self.cancelled {
                    imageData = nil
                    return
                }
    
                // scale image
                if imageData != nil {
                    if let image = UIImage(data: imageData!) {
                        let imageData2 = UIImageJPEGRepresentation(image, 1.0)
                        let compressionRate = Float(imageData!.length) / Float(imageData2!.length)
    
                        let scaleWidth = 244 / image.size.width
                        let scaleHeight = 244 / image.size.height
                        let imageScale = min(scaleWidth, scaleHeight)
    
                        let rect = CGRectMake(0.0, 0.0, image.size.width * imageScale, image.size.height * imageScale)
    
                        UIGraphicsBeginImageContext(rect.size)
                        image.drawInRect(rect)
                        let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
                        let scaledImageData = UIImageJPEGRepresentation(scaledImage, CGFloat(compressionRate))
                        UIGraphicsEndImageContext()
    
                        imageData = scaledImageData
                    }
                }
            }
        }
    
        private func setNetworkActivityIndicatorVisible(visible: Bool) {
            NSOperationQueue.mainQueue().addOperationWithBlock() {
                let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
                appDelegate.setNetworkActivityIndicatorVisible(visible)
            }
        }
    }
    

    Where exactly do I create the retain cycle? And how do I solve this?
    When should I use ‘unowned’ and when should I use ‘weak’?

    I would appreciate it if someone can explain the solution, so I can learn from my mistake.

    Solutions Collect From Internet About “Swift: Retain cycle with NSOperation”

    I found the problem. The retain cycle is not caused by referencing self, but by referencing the NSOperation in the completion block of the NSOperation!

    In the function startDownloadForUrl(…) I declare the variable downloader. Next I declare an completion block for this variable. In this completion block I reference the variable downloader. This causes the retain cycle.

    I solved this by using [unowned downloader] within the completion block.

    This created anaother problem. In the completion block I asynchronously call the main thread. In this call the variable downloader.imageData was used. Because of this asynchronous call, the NSOperation may be already ended and the variable downloader may not longer exists. To avoid crashes I declare a new variable for the imageData, so the data will still be available when used in the main thread.

    The completion block now looks like:

    downloader.completionBlock = {
        [unowned downloader] in
        if downloader.cancelled {
            return
        }
    
        let imageData = downloader.imageData    // retain the imageData. It will be used asynchrounous in the main thread. The downloader operation might already be finished and downloader will no longer exists.
    
        // image is retrieved from web
        NSOperationQueue.mainQueue().addOperationWithBlock() {
            //remove indexpath from progress queue
            self.downloadsInProgress.removeValueForKey(indexPath)
    
            // add image to cache
            if imageData != nil {
                self.cache.setObject(imageData!, forKey: url, cost: imageData!.length)
            }
            completion(imageData: imageData)
        }
    }