QLThumbnailGenerator loading wrong images

I have an NSCollectionView which is showing thumbnails of images and I have just switched to using QLThumbnailGenerator to fetch them.

There are over 6,000 possible images that can be viewed and, if I scroll too fast, I start to get the wrong thumbnails returned from the generator.

Is this a bug, or is there something I can do to fix this?

Here is the code that is written inside the NSCollectionViewItem derived class…

  var request: QLThumbnailGenerator.Request?
  
  func loadImage()
  {
    if imageView?.image != NSImage(imageLiteralResourceName: "Placeholder")
    {
      return
    }
    
    request = QLThumbnailGenerator.Request(fileAt: url!, size: imageSize, scale: 1.0, representationTypes: [.lowQualityThumbnail])
    
  QLThumbnailGenerator.shared.generateBestRepresentation(for: request!)
    {
      (thumbnail: QLThumbnailRepresentation?, error: Error?) -> Void in
      
      if let request = self.request
      {
        QLThumbnailGenerator.shared.cancel(request)
      }
      
      DispatchQueue.main.async
      {
        [unowned self] in
        
        if self.imageView?.image != NSImage(imageLiteralResourceName: "Placeholder")
        {
          return
        }
        
        let transition = CATransition()
        
        transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
        
        transition.duration = 0.3
        
        imageView?.layer?.add(transition, forKey: nil)
        
        imageView?.image = thumbnail?.nsImage

        …

  • Does nobody have an answer to this question?

Add a Comment

Replies

Well, since nobody else seems to be able to answer this question, I will do it myself…

It turns out I was using the wrong QLThumbnailGenerator.

  override func prepareForReuse()
  {
    super.prepareForReuse()
    
    if let request = self.request
    {
      thumbnailGenerator.cancel(request)

      self.request = nil
    }
    
    imageView?.image = NSImage(imageLiteralResourceName: "Placeholder")

The problem was so subtle. One thing I didn't quote in my original post was that I had a var that holds a private QLThumbnailGenerator instance and it was this that I was using to cancel the request, even though I was using the shared instance in the loadImage method.

As soon as I switched the loadImage method to use the private var, everything started to work better.

I say better because there was still the occasion when the generator would return a nil image for some obscure reason. So, for those times, I have a helper method that loads those thumbnails the "old fashioned" way…

  static func previewOfFile(at url: URL, size: NSSize, asIcon: Bool = false, completion: @escaping (NSImage) -> ())
  {
    let cfUrl = url as CFURL
    
    guard let imageSource = CGImageSourceCreateWithURL(cfUrl, nil),
          let image = getImage(for: imageSource, at: url, size: size) else
    {
      completion(NSWorkspace.shared.icon(forFile: url.path))
      
      return
    }
    
    completion(image)
  }

… and the full code now reads…

  let thumbnailGenerator = QLThumbnailGenerator()
  
  var request: QLThumbnailGenerator.Request?
  
  func loadImage()
  {
    imageView?.image = NSImage(imageLiteralResourceName: "Placeholder")
    
    request = QLThumbnailGenerator.Request(fileAt: self.url!, size: self.imageSize, scale: 1.0, representationTypes: [.lowQualityThumbnail])
    
    self.thumbnailGenerator.generateRepresentations(for: self.request!)
    {
      (thumbnail: QLThumbnailRepresentation?, type: QLThumbnailRepresentation.RepresentationType, error: Error?) -> Void in
      
      DispatchQueue.main.async
      {
        let transition = CATransition()
        
        transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
        
        transition.duration = 0.5
        
        self.imageView?.layer?.add(transition, forKey: nil)
        
        self.imageView?.image = thumbnail?.nsImage
        
        if thumbnail?.nsImage == nil
        {
          DispatchQueue.main.async
          {
            ThumbnailImage.previewOfFile(at: self.url!, size: self.imageSize)
            {
              [unowned self] retrievedImage in
              
              transition.duration = 0.3
              
              self.imageView?.image = retrievedImage
            }
          }
        }

I hope this is of use to anyone else who stumbles across this issue