Episode #177

NSOperation Dependencies

Series: Deep Dive with NSOperation

11 minutes
Published on July 9, 2015

This video is only available to subscribers. Get access to this video and 586 others.

In this episode we discuss dependent NSOperations. Using dependencies you can queue up a bunch of jobs and they will be run in the correct order, having one provide the necessary state for the next one to run. In the example, we take the large Hubble image, scale it down to a more appropriate size, then apply a Core Image filter to it. Each operation is dependent on the one before it, yet they are all queued up at the same time.

Episode Links

Creating a resize operation

class ResizeImageOperation : Operation {
    let path: String
    let containingSize: CGSize

    init(path: String, containingSize: CGSize) {
        self.path = path
        self.containingSize = containingSize
    }

    override func execute() {
        let sourceImage = UIImage(contentsOfFile: path)!
        println("Source image size: \(sourceImage.size)")
        printSizeOnDisk(UIImageJPEGRepresentation(sourceImage, 1.0))

        let width: CGFloat
        let height: CGFloat
        let ratio = sourceImage.size.width / sourceImage.size.height
        if sourceImage.size.width >= sourceImage.size.height {
            width = containingSize.width
            height = width / ratio
        } else {
            height = containingSize.height
            width = height * ratio
        }

        let imageSize = CGSize(width: width, height: height)
        println("Resized image: \(imageSize)")

        UIGraphicsBeginImageContextWithOptions(imageSize, true, 0.0)
        let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: imageSize)
        sourceImage.drawInRect(rect)
        let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        let imageData = UIImageJPEGRepresentation(resizedImage, 0.8)
        printSizeOnDisk(imageData)
        imageData.writeToFile(path, atomically: true)

        finish()
    }

    func printSizeOnDisk(data: NSData) {
        let bytes = Int64(data.length)
        let size = NSByteCountFormatter.stringFromByteCount(bytes, countStyle: NSByteCountFormatterCountStyle.File)
        println("Size on disk: \(size)")
    }
}

Creating a filter operation

class FilterImageOperation : Operation {
    let path: String
    var outputImage: CIImage?

    init(path: String) {
        self.path = path
    }

    lazy var inputImage: CIImage = {
        let url = NSURL(fileURLWithPath: self.path)!
        return CIImage(contentsOfURL: url)
    }()

    lazy var filter: CIFilter = {
        return CIFilter(name: "CISepiaTone", withInputParameters: [
            kCIInputImageKey : self.inputImage
        ])
    }()

    override func execute() {
        outputImage = filter.outputImage
        finish()
    }
}

Setting up the dependencies

@IBAction func downloadButtonTapped(sender: AnyObject) {

        let url = NSURL(string: "http://imgsrc.hubblesite.org/hu/db/images/hs-2006-10-a-hires_jpg.jpg")!

        let docsDir = NSSearchPathForDirectoriesInDomains(.CachesDirectory, .UserDomainMask, true).first! as! String
        let targetPath = docsDir.stringByAppendingPathComponent("hubble.jpg")

        let downloadOperation = DownloadImageOperation(imageURL: url, targetPath: targetPath)

        downloadOperation.progressBlock = { self.downloadProgressView.progress = $0 }

        var size = CGSize(width: imageView.bounds.size.width * 2, height: imageView.bounds.size.height * 2)
        let resizeOperation = ResizeImageOperation(path: targetPath, containingSize: size)
        resizeOperation.addDependency(downloadOperation)

        let filterOperation = FilterImageOperation(path: targetPath)
        filterOperation.addDependency(resizeOperation)

        // Thanks to Derek in the comments for pointing out the retain cycle here. Updated to use a weak reference.
        filterOperation.completionBlock = { [weak filterOperation] in
            if let outputImage = filterOperation?.outputImage {
                let context = CIContext(options: [:])
                let cgImage = context.createCGImage(outputImage, fromRect: outputImage.extent())
                let image = UIImage(CGImage: cgImage)!
                self.displayImage(image)
            }
        }

        operationQueue.suspended = true
        operationQueue.addOperation(downloadOperation)
        operationQueue.addOperation(resizeOperation)
        operationQueue.addOperation(filterOperation)

        operationQueue.suspended = false
    }