Welkin's Secret Garden

Adapt Cell Height to Web Image

There is a requirement to show a list of images that fetched from the internet, such as the scene of product’s detail in Taobao app. Have a glance of the effect first.

(The gif is a little large.)

Each cell contains simply an image view that covers the whole cell. The difficulty is that before we have got the image, we do not know the proportion of its width and height, making it hard to define the height of cells. However, the table view should be loaded at once in order to put some place holders on it.

Thus, we should divide the whole process into two parts:

  1. Show the place holder and to download the image.
  2. Reload to show the image when appropriate.

I would like to choose YYWebImage as a web image loading framework. Of course, there are other choices and I just take an example.

The Core

The downloaded images will be stored in a NSMutableDictionary or NSCache and the keys are the index of the cell so we can ignore the order of downloading. If the wanted data is ready, return the data. Or, just return the place holder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BBProductBoxImageCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([BBProductBoxImageCell class])];
CGFloat height = [self.cellHeights[@(indexPath.row)] floatValue];
if (height > 0) {
cell.innerImageView.image = self.cellImages[@(indexPath.row)];
return cell;
}
cell.innerImageView.image = [UIImage imageNamed:@"pic_moren_225"];
[[YYWebImageManager sharedManager] requestImageWithURL:[NSURL URLWithString:self.imgs[indexPath.row] ] options:YYWebImageOptionSetImageWithFadeAnimation progress:nil transform:nil completion:^(UIImage * _Nullable image, NSURL * _Nonnull url, YYWebImageFromType from, YYWebImageStage stage, NSError * _Nullable error) {
if (error) {
return;
}
self.cellImages[@(indexPath.row)] = image;
self.cellHeights[@(indexPath.row)] = @(image.size.height * [UIScreen mainScreen].bounds.size.width / image.size.width);
}];
return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat height = [self.cellHeights[@(indexPath.row)] floatValue];
return height > 0 ? height : 300;
}

When to reload

We now know how to determine whether to return the place holder and download the image or return the downloaded image directly. But when is the appropriate timeing to reload the table view in order to replace the place holder to the wanted image?

The easiest way is to reload the corresponding index path as soon as the image is downloaded.

1
2
3
4
5
6
7
self.cellImages[@(indexPath.row)] = image;
self.cellHeights[@(indexPath.row)] = @(image.size.height * [UIScreen mainScreen].bounds.size.width / image.size.width);
dispatch_async(dispatch_get_main_queue(), ^{
[UIView performWithoutAnimation:^{
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}];
});

But in this way, the content offset jumps randomly even when I am dragging the table view. Thus I add a property to mark whether the table view should be reloaded. (If the table view is not being draging, just reload it at once)

1
2
3
4
5
6
7
8
9
10
11
self.cellImages[@(indexPath.row)] = image;
self.cellHeights[@(indexPath.row)] = @(image.size.height * [UIScreen mainScreen].bounds.size.width / image.size.width);
if (tableView.isDragging) {
self.needRefresh = YES;
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[UIView performWithoutAnimation:^{
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}];
});

Then reload it when it stop moving.

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if (self.needRefresh) {
[self.tableView reloadData];
self.needRefresh = NO;
}
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (self.needRefresh) {
[self.tableView reloadData];
self.needRefresh = NO;
}
}

That’s for today.