Image download and cache

I have a list of 5000 lines that stores url of images. When the list is loaded, links are stored inside dataItem.logo as Strings.

What is the best approach? Should images be downloaded and cached one by one when the list is loaded or to do this when a cell is displayed in UICollectionView?

I'm thinking that more than half of the images will never be displayed anyway and Im trying the second way.

I have this code from UIKit:

class ChannelCellComposer {
    // MARK: Properties
    
    /// Cache used to store processed images, keyed on `DataItem` identifiers.
    static private var processedImageCache = NSCache<NSString, UIImage>()
    
    /**
        A dictionary of `NSOperationQueue`s for `DataItemCollectionViewCell`s. The
        queues contain operations that process images for `DataItem`s before updating
        the cell's `UIImageView`.
    */
    private var operationQueues = [ChannelCollectionViewCell: OperationQueue]()

    // MARK: Implementation
    
    func compose(_ cell: ChannelCollectionViewCell, withDataItem dataItem: Song) {
        // Cancel any queued operations to process images for the cell.
        let queue = operationQueue(forCell: cell)
        queue.cancelAllOperations()
        
        // Set the cell's properties.
        cell.representedDataItem = dataItem
        cell.label.text = dataItem.title
        cell.imageView.alpha = 1.0
        cell.imageView.image = ChannelCellComposer.processedImageCache.object(forKey: dataItem.logo as NSString)
        
        print(dataItem.logo)
        
        // No further work is necessary if the cell's image view has an image.
        guard cell.imageView.image == nil else { return }
        

        /*
            Initial rendering of a jpeg image can be expensive. To avoid stalling
            the main thread, we create an operation to process the `DataItem`'s
            image before updating the cell's image view.
        
            The execution block is added after the operation is created to allow
            the block to check if the operation has been cancelled.
        */
        let processImageOperation = BlockOperation()
        
        processImageOperation.addExecutionBlock { [unowned processImageOperation] in
            // Ensure the operation has not been cancelled.
            guard !processImageOperation.isCancelled else { return }
            
            // Load and process the image.
            guard let image = self.processImage(named: dataItem.logo) else { return }
            
            // Store the processed image in the cache.
            ChannelCellComposer.processedImageCache.setObject(image, forKey: dataItem.logo as NSString)
            
            OperationQueue.main.addOperation {
                // Check that the cell is still showing the same `DataItem`.
                guard dataItem == cell.representedDataItem else { return }

                // Update the cell's `UIImageView` and then fade it into view.
                cell.imageView.alpha = 0.0
                cell.imageView.image = image
                
                UIView.animate(withDuration: 0.25) {
                    cell.imageView.alpha = 1.0
                }
            }
        }
        
        queue.addOperation(processImageOperation)
    }
    
    // MARK: Convenience
    
    /**
        Returns the `NSOperationQueue` for a given cell. Creates and stores a new
        queue if one doesn't already exist.
    */
    private func operationQueue(forCell cell: ChannelCollectionViewCell) -> OperationQueue {
        if let queue = operationQueues[cell] {
            return queue
        }
        
        print("operationQueue")
        
        let queue = OperationQueue()
        operationQueues[cell] = queue
        
        return queue
    }
    
    /**
        Loads a UIImage for a given name and returns a version that has been drawn
        into a `CGBitmapContext`.
    */
    private func processImage(named imageName: String) -> UIImage? {
        // Load the image.
        guard let image = UIImage(named: imageName) else { return nil }
        
        print("process Imaage")
        
        /*
            We only need to process jpeg images. Do no processing if the image
            name doesn't have a jpg suffix.
        */
//        guard imageName.hasSuffix(".jpg") else { return image }
        
        // Create a `CGColorSpace` and `CGBitmapInfo` value that is appropriate for the device.
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
        
        // Create a bitmap context of the same size as the image.
        let imageWidth = Int(Float(image.size.width))
        let imageHeight = Int(Float(image.size.height))
        
        let bitmapContext = CGContext(data: nil, width: imageWidth, height: imageHeight, bitsPerComponent: 8, bytesPerRow: imageWidth * 4, space: colorSpace, bitmapInfo: bitmapInfo)
        
        // Draw the image into the graphics context.
        guard let imageRef = image.cgImage else { fatalError("Unable to get a CGImage from a UIImage.") }
        bitmapContext?.draw(imageRef, in: CGRect(origin: CGPoint.zero, size: image.size))
        
        // Create a new `CGImage` from the contents of the graphics context.
        guard let newImageRef = bitmapContext?.makeImage() else { return image }
        
        // Return a new `UIImage` created from the `CGImage`.
        return UIImage(cgImage: newImageRef)
    }
}

What I want to do for now is to download the image from url and then replace the string from dataItem.logo with the image object. I think downloading will be done by using try? Data(contentsOf: dataItem.logo)
but how can I replace the dataItem.logo (String) with dataItem.logo (image)

There are a variety of libraries out there that can do this sort of loading for you (Kingfisher, SDWebImage, AlamofireImage). I would start with one of those.

If you insist on doing this yourself, at the very least I would get rid of the 1:1 OperationQueue mapping you have going on, as it's going to consume a lot of system resources, especially if you're queueing up 5000 items at a time. Instead, a single queue should be used. I would start with a serial queue (maxConcurrentOperationCount = 1) and test from there to see what gets you the best performance. You'll likely be surprised just how little concurrency you really need.

I also don't understand what you're doing in processImage, as you're loading the UIImage and loading it through a CGBitmapContext, which seems pointless, since you're not even resizing the image or anything. Everything after loading the initial UIImage seems redundant there, unless I'm missing something.

1 Like

I want to keep it simple and use as less libraries as possible. I will remove processImage, I was just thinking that I may will need it later.

The problem now is that dataItem.logo is still a string and not becomes a image that can be displayed.

I thought you already had the network loading and just left it out of your example. But yes, you'll need to download the files to disk to work with the code you've shown so far. I'm pretty sure UIImage(named:) won't work for files like that, you'll need to use UIImage(contentsOfFile:) instead.

1 Like

I just have one more question, how can I save cached image to persistentstorage?

You can use the FileManager to move the downloaded files.
You can also query the FileManager to get a path or URL pointing to the location where you can store the images. Commonly you would store cached data in the .cachesDirectory, since the data can always be downloaded again later.
Note that the OS may purge contents of the caches directory at any time, so your code should be able to redownload some or all the images.
The FileManager documentation has more details about the various content locations.

1 Like

I get lots of issues when I try to convert cache method from NSCache to CoreData.
There are actually 2 lines of code that I don't know to replace.

class MovieCellComposer {
    // MARK: Properties
    
    /// Cache used to store processed images, keyed on `DataItem` identifiers.
    static private var processedImageCache = NSCache<NSString, UIImage>() <---- first line
    
    /**
     A dictionary of `NSOperationQueue`s for `DataItemCollectionViewCell`s. The
     queues contain operations that process images for `DataItem`s before updating
     the cell's `UIImageView`.
     */
    private var operationQueues = [MoviesCollectionViewCell: OperationQueue]()
    
    // MARK: Implementation
    
    func compose(_ cell: MoviesCollectionViewCell, withDataItem dataItem: Item) {
        // Cancel any queued operations to process images for the cell.
        let queue = operationQueue(forCell: cell)
        queue.cancelAllOperations()
        
        // Set the cell's properties.
        cell.representedDataItem = dataItem
        cell.textlabel.text = dataItem.title
        cell.imageView.alpha = 1.0
        cell.imageView.image = MovieCellComposer.processedImageCache.object(forKey: dataItem.logo! as NSString) <----- second line
        
        // No further work is necessary if the cell's image view has an image.
        guard cell.imageView.image == nil else { return }
        print("guard cell.imageView.image == nil else { return } // No further work is necessary if the cell's image view has an image.")
        
        /*
         Initial rendering of a jpeg image can be expensive. To avoid stalling
         the main thread, we create an operation to process the `DataItem`'s
         image before updating the cell's image view.
         
         The execution block is added after the operation is created to allow
         the block to check if the operation has been cancelled.
         */
        let processImageOperation = BlockOperation()
        
        processImageOperation.addExecutionBlock { [unowned processImageOperation] in
            // Ensure the operation has not been cancelled.
            guard !processImageOperation.isCancelled else { return }
            
            // Load and process the image.
//            guard let image = self.processImage(named: dataItem.logo) else { return }
                        
            SearchMDB.movie(query: dataItem.title!, language: "en", page: 1, includeAdult: true, year: nil, primaryReleaseYear: nil){
                data, movies in
                
                guard ((movies?.first) != nil) else { return }
                
                find = movies?.first
                
                guard find.backdrop_path != nil else { return }
                print(baseURL + find.poster_path!)
                
                let imageURL = baseURL + find.poster_path!
                
                let data = try! Data(contentsOf: URL(string: imageURL)!)
                //            self.imageView.image = UIImage(data: data)
             
                guard let image = UIImage(data: data) else { return }
                
            
                print("LOADING A NEW IMAGE")
                
                // Store the processed image in the cache.
                MovieCellComposer.processedImageCache.setObject(image, forKey: dataItem.logo! as NSString) <--------
/*
I think this should be:
                dataItem.logo = data
                saveToPersistentStore()
*/
                
                OperationQueue.main.addOperation {
                    // Check that the cell is still showing the same `DataItem`.
                    guard dataItem == cell.representedDataItem else { return }
                    
                    // Update the cell's `UIImageView` and then fade it into view.
                    cell.imageView.alpha = 0.0
                    cell.imageView.image = image
                    
                    UIView.animate(withDuration: 0.25) {
                        cell.imageView.alpha = 1.0
                    }
                }
            }
        }
        
        queue.addOperation(processImageOperation)
    }
    
    // MARK: Convenience
    
    /**
     Returns the `NSOperationQueue` for a given cell. Creates and stores a new
     queue if one doesn't already exist.
     */
    private func operationQueue(forCell cell: MoviesCollectionViewCell) -> OperationQueue {
        if let queue = operationQueues[cell] {
            return queue
        }
        
        let queue = OperationQueue()
        operationQueues[cell] = queue
        
        return queue
    }
}