HTML and CSS   XSLT   JavaScript   Images   Soft   Etc  
Alexey Kuznetsov

Scrolling a large table with images in iOS April 26, 2011


Task:

to ensure smooth scrolling of a table with large images.

Let’s take a UITableView table which is used as a contents page for an illustrated iPad book. Each table cell contains several scaled-down pages of the book. These images are fairly large since it’s a comic book and users will be using the thumbnails to find a specific page.




When the image is large, decoding it takes a lot of time. This is also the case with PNG which at compilation stage is being converted to a format convenient for the device and is recommended by Apple for use in UIKit-based programs. Without special considerations there is no way to achieve smooth scrolling of a table that contains such large images.

At first it might seem that the problem is in reading the file from media, but the profiler confirms it isn’t so. Both -[UIImage imageNamed:] and -[UIImage imageWithContentsOfFile:] do not decode images instantly and postpone it for later. Decoding can happen for example when -[UIImageView setImage:] is called. And the worst thing is that there is no way to call this method outside of the main thread. Before iOS 4 no UIKit methods could be called in non-main threads. In iOS 4 the situation is slightly different, but in relation to -[UIImageView setImage:] the rules remain the same. It turns out that while operating on the UIKit level the only way to run a resource-consuming image decode operation is to do it in the precious main thread which is busy scrolling the table right when this image is required.

Of course, sometimes it’s possible to decode all the images before serving them. But such a solution is unreliable since there can be lots of images and they all might not fit into memory. And all the same, we would still have to spend the main thread time at one point or another. If we decide to do it at app launch, it will lead to increased app start time which is not something we can afford in the case of iOS.

Thankfully, we have at our disposal not only UIKit with UIImage but also Core Graphics with CGImage, and CGImage functions can be called in background, outside of the main thread. Moreover, this is also where we can decode an image. Here is how it’s done:

01 
02 
03 
04 
05 
06 
07 
08 
09 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
UIImage *anImage = ...  // Assume this exists.
CGImageRef originalImage = (CGImageRef)anImage;
assert(originalImage != NULL);

CFDataRef imageData = CGDataProviderCopyData(CGImageGetDataProvider(originalImage));
CGDataProviderRef imageDataProvider = CGDataProviderCreateWithCFData(imageData);
if (imageData != NULL) {
  CFRelease(imageData);
}
CGImageRef image = CGImageCreate(CGImageGetWidth(originalImage),
                                 CGImageGetHeight(originalImage),
                                 CGImageGetBitsPerComponent(originalImage),
                                 CGImageGetBitsPerPixel(originalImage),
                                 CGImageGetBytesPerRow(originalImage),
                                 CGImageGetColorSpace(originalImage),
                                 CGImageGetBitmapInfo(originalImage),
                                 imageDataProvider,
                                 CGImageGetDecode(originalImage),
                                 CGImageGetShouldInterpolate(originalImage),
                                 CGImageGetRenderingIntent(originalImage));
if (imageDataProvider != NULL) {
  CGDataProviderRelease(imageDataProvider);
}

// Do something with the image.

CGImageRelease(image);

The image received from CGImageCreate() is already decoded, which is exactly what we need. All that’s left is to wrap this code in NSOperation to execute it in background and finally send the results to the main thread. Using +[UIImage imageWithCGImage:] we get UIImage from the resulting CGImage and assign it to the required UIImageView right during scrolling. The animation works flawlessly.

Usage example:

01 
02 
03 
04 
05 
06 
07 
08 
09 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67 
68 
69 
70 
71 
72 
73 
74 
75 
76 
77 
78 
79 
80 
81 
82 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  NSUInteger index = ...;  // Page index, assume it exists.
  
  NSNumber *indexNumber = [NSNumber numberWithUnsignedInteger:index];
  UIImage *thumbnailImage = [self thumbnailForPageAtIndex:index];
  CGImageRef imageRef = [thumbnailImage CGImage];
  NSDictionary *thumbnailDict = [NSDictionary dictionaryWithObjectsAndKeys:
                                 indexNumber, kThumbnailIndexKey,
                                 imageRef, kThumbnailImageKey,
                                 nil];
  
  NSInvocationOperation *thumbnailOperation
    = [[[NSInvocationOperation alloc]
        initWithTarget:self
              selector:@selector(decodeThumbnail:)
                object:thumbnailDict]
       autorelease];
  
  [[self operationQueue] addOperation:thumbnailOperation];
}

- (void)decodeThumbnail:(NSDictionary *)thumbnailDict {
  // thumbnailDict contains thumbnail and its index. Thumbnail
  // is represented by a UIImage object with key kThumbnailImageKey,
  // Thumbnail index is represented by an NSNumber object with key
  // kThumbnailIndexKey.
  
  CGImageRef originalImage = (CGImageRef)[thumbnailDict objectForKey:kThumbnailImageKey];
  NSNumber *indexNumber = [thumbnailDict objectForKey:kThumbnailIndexKey];
  if (originalImage == NULL || indexNumber == nil) {
    return;
  }
  
  CFDataRef imageData = CGDataProviderCopyData(CGImageGetDataProvider(originalImage));
  CGDataProviderRef imageDataProvider = CGDataProviderCreateWithCFData(imageData);
  if (imageData != NULL) {
    CFRelease(imageData);
  }
  CGImageRef image = CGImageCreate(CGImageGetWidth(originalImage),
                                   CGImageGetHeight(originalImage),
                                   CGImageGetBitsPerComponent(originalImage),
                                   CGImageGetBitsPerPixel(originalImage),
                                   CGImageGetBytesPerRow(originalImage),
                                   CGImageGetColorSpace(originalImage),
                                   CGImageGetBitmapInfo(originalImage),
                                   imageDataProvider,
                                   CGImageGetDecode(originalImage),
                                   CGImageGetShouldInterpolate(originalImage),
                                   CGImageGetRenderingIntent(originalImage));
  if (imageDataProvider != NULL) {
    CGDataProviderRelease(imageDataProvider);
  }
  
  NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
                        indexNumber, kThumbnailIndexKey,
                        image, kThumbnailImageKey,
                        nil];
  CGImageRelease(image);
  
  [self performSelectorOnMainThread:@selector(didDecodeThumbnail:)
                         withObject:dict
                      waitUntilDone:NO];
}

- (void)didDecodeThumbnail:(NSDictionary *)thumbnailDict {
  // Unpack thumbnail and its index.
  NSNumber *indexNumber = [thumbnailDict objectForKey:kThumbnailIndexKey];
  NSUInteger index = [indexNumber unsignedIntegerValue];
  CGImageRef imageRef = (CGImageRef)[thumbnailDict objectForKey:kThumbnailImageKey];
  UIImage *image = [UIImage imageWithCGImage:imageRef];
  
  // Set thumbnail as an image for the button that opens book page at a given index.
  NSUInteger row = index / kThumbnailsInRow;
  NSIndexPath *path = [NSIndexPath indexPathForRow:row inSection:0];
  NSArray *visiblePaths = [[self tableView] indexPathsForVisibleRows];
  if ([visiblePaths containsObject:path]) {
    UITableViewCell *cell = [[self tableView] cellForRowAtIndexPath:path];
    NSInteger tag = index - row * kThumbnailsInRow + 1;
    UIButton *button = (UIButton *)[cell viewWithTag:tag];
    [button setImage:image forState:UIControlStateNormal];
  }
}

Order a design...