Search code examples
objective-cperformanceuicollectionviewrubymotionuicollectionviewcell

Poor UICollectionView Scrolling Performance With UIImage


I have a UICollectionView in my app, and each cell is a UIImageView and some text labels. The problem is that when I have the UIImageViews displaying their images, the scrolling performance is terrible. It's nowhere near as smooth as the scrolling experience of a UITableView or even the same UICollectionView without the UIImageView.

I found this question from a few months ago, and it seems like an answer was found, but it's written in RubyMotion, and I don't understand that. I tried to see how to convert it to Xcode, but since I have never used NSCache either, it's a little hard to. The poster there also pointed to here about implementing something in addition to their solution, but I'm not sure where to put that code either. Possibly because I don't understand the code from the first question.

Would someone be able to help translate this into Xcode?

def viewDidLoad
  ...
  @images_cache = NSCache.alloc.init
  @image_loading_queue = NSOperationQueue.alloc.init
  @image_loading_queue.maxConcurrentOperationCount = 3
  ...
end

def collectionView(collection_view, cellForItemAtIndexPath: index_path)
  cell = collection_view.dequeueReusableCellWithReuseIdentifier(CELL_IDENTIFIER, forIndexPath: index_path)
  image_path = @image_paths[index_path.row]

  if cached_image = @images_cache.objectForKey(image_path)
    cell.image = cached_image
  else
    @operation = NSBlockOperation.blockOperationWithBlock lambda {
      @image = UIImage.imageWithContentsOfFile(image_path)
      Dispatch::Queue.main.async do
        return unless collectionView.indexPathsForVisibleItems.containsObject(index_path)
        @images_cache.setObject(@image, forKey: image_path)
        cell = collectionView.cellForItemAtIndexPath(index_path)
        cell.image = @image
      end
    }
    @image_loading_queue.addOperation(@operation)
  end
end

Here is the code from the second question that the asker of the first question said solved the problem:

UIImage *productImage = [[UIImage alloc] initWithContentsOfFile:path];

CGSize imageSize = productImage.size;
UIGraphicsBeginImageContext(imageSize);
[productImage drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)];
productImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

Again, I'm not sure how/where to implement that.

Many thanks.


Solution

  • Here's the pattern I follow. Always load asynch and cache the result. Make no assumption about the state of the view when the asynch load finishes. I have a class that simplifies the loads as follows:

    //
    //  ImageRequest.h
    
    // This class keeps track of in-flight instances, creating only one NSURLConnection for
    // multiple matching requests (requests with matching URLs).  It also uses NSCache to cache
    // retrieved images.  Set the cache count limit with the macro in this file.
    
    #define kIMAGE_REQUEST_CACHE_LIMIT  100
    typedef void (^CompletionBlock) (UIImage *, NSError *);
    
    @interface ImageRequest : NSMutableURLRequest
    
    - (UIImage *)cachedResult;
    - (void)startWithCompletion:(CompletionBlock)completion;
    
    @end
    
    //
    //  ImageRequest.m
    
    #import "ImageRequest.h"
    
    NSMutableDictionary *_inflight;
    NSCache *_imageCache;
    
    @implementation ImageRequest
    
    - (NSMutableDictionary *)inflight {
    
        if (!_inflight) {
            _inflight = [NSMutableDictionary dictionary];
        }
        return _inflight;
    }
    
    - (NSCache *)imageCache {
    
        if (!_imageCache) {
            _imageCache = [[NSCache alloc] init];
            _imageCache.countLimit = kIMAGE_REQUEST_CACHE_LIMIT;
        }
        return _imageCache;
    }
    
    - (UIImage *)cachedResult {
    
        return [self.imageCache objectForKey:self];
    }
    
    - (void)startWithCompletion:(CompletionBlock)completion {
    
        UIImage *image = [self cachedResult];
        if (image) return completion(image, nil);
    
        NSMutableArray *inflightCompletionBlocks = [self.inflight objectForKey:self];
        if (inflightCompletionBlocks) {
            // a matching request is in flight, keep the completion block to run when we're finished
            [inflightCompletionBlocks addObject:completion];
        } else {
            [self.inflight setObject:[NSMutableArray arrayWithObject:completion] forKey:self];
    
            [NSURLConnection sendAsynchronousRequest:self queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
                if (!error) {
                    // build an image, cache the result and run completion blocks for this request
                    UIImage *image = [UIImage imageWithData:data];
                    [self.imageCache setObject:image forKey:self];
    
                    id value = [self.inflight objectForKey:self];
                    [self.inflight removeObjectForKey:self];
    
                    for (CompletionBlock block in (NSMutableArray *)value) {
                        block(image, nil);
                    }
                } else {
                    [self.inflight removeObjectForKey:self];
                    completion(nil, error);
                }
            }];
        }
    }
    
    @end
    

    Now the cell (collection or table) update is fairly simple:

    -(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    
        UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
    
        NSURL *url = [NSURL URLWithString:@"http:// some url from your model"];
        // note that this can be a web url or file url
    
        ImageRequest *request = [[ImageRequest alloc] initWithURL:url];
    
        UIImage *image = [request cachedResult];
        if (image) {
            UIImageView *imageView = (UIImageView *)[cell viewWithTag:127];
            imageView.image = image;
        } else {
            [request startWithCompletion:^(UIImage *image, NSError *error) {
                if (image && [[collectionView indexPathsForVisibleItems] containsObject:indexPath]) {
                    [collectionView reloadItemsAtIndexPaths:@[indexPath]];
                }
            }];
        }
        return cell;
    }