0

In my iOS app, I have a UICollectionView where each cell contains an image. To prevent the view from taking a long time to load, I load each with a blank image and title before loading the image contents in a background task.

I logged which images are getting loaded through in the background async task, and it seems like the images of cells off screen get loaded first, followed by the cells at the top of the screen. This makes the app seem unresponsive, and I'd rather have the cells at the top take priority in terms of loading:

enter image description here

I also notice that once I start scrolling, the images in the cells suddenly start appearing, but they take much longer to appear on their own. Can anyone suggest strategies to control the ordering that UICollectionCells load in?

Here is my code:

Iterate over projects and add imageViews to an NSMutableArray projectContainers, which then gets turned into cells

for (NSDictionary *currentProject in projects)
    {
        // data entry
        [projectIDs addObject: [currentProject objectForKey:@"id"]];
        NSString *projectTitle = [currentProject objectForKey:@"title"];

        id delegate = [[UIApplication sharedApplication] delegate];
        self.managedObjectContext = [delegate managedObjectContext];

        CustomLabel *cellLabel=[[CustomLabel alloc]init];
        cellLabel.text = trimmedProjectTitle;
        [titles addObject:projectTitle];

        CGSize maxLabelSize = CGSizeMake(cellWidth,100);

        CustomLabel *titleLabel = [[CustomLabel alloc]init]; 
        // titleLabel styling
        titleLabel.backgroundColor = [[UIColor blackColor]colorWithAlphaComponent:0.5f];
        titleLabel.textColor =[UIColor whiteColor];
        [titleLabel setFont: [UIFont fontWithName: @"HelveticaNeue" size:12]];

        titleLabel.text = trimmedProjectTitle;
        CGSize expectedLabelSize = [titleLabel.text sizeWithFont:titleLabel.font constrainedToSize:maxLabelSize lineBreakMode:NSLineBreakByWordWrapping];

        CGRect labelFrame = (CGRectMake(0, 0, cellWidth, 0));
        labelFrame.origin.x = 0;
        labelFrame.origin.y = screenWidth/2 - 80 - expectedLabelSize.height;
        labelFrame.size.height = expectedLabelSize.height+10;
        titleLabel.frame = labelFrame;

        // add placeholder image with textlabel
        UIImageView *imagePreview = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, cellWidth, cellHeight)];
        imagePreview.contentMode= UIViewContentModeScaleAspectFill;
        imagePreview.clipsToBounds = YES;
        [imagePreview setImage:[UIImage imageNamed:@"blank.png"]];
        [imagePreview addSubview:titleLabel];
        [imagePreview.subviews[0] setClipsToBounds:YES];
        [projectContainers addObject: imagePreview];

        // add project thumbnail images in async
        dispatch_async(bgQueue, ^{
          NSDictionary *imagePath = [currentProject objectForKey:@"image_path"];
          NSString *imageUrlString = [imagePath objectForKey: @"preview"];
          NSURL *imageUrl = [NSURL URLWithString: imageUrlString];
          NSData *imageData = [[NSData alloc] initWithContentsOfURL:(imageUrl)];
          UIImage *image = [[UIImage alloc] initWithData:(imageData)];
          if(image){
              NSLog(@"project with image: %@", projectTitle);
              [imagePreview setImage: image];
          }
            BOOL *builtVal = [[currentProject objectForKey:@"built"]boolValue];
            if(builtVal){
                UIImageView *builtBanner =[[UIImageView alloc]initWithImage:[UIImage imageNamed:@"built_icon.png"]];
                builtBanner.frame = CGRectMake(screenWidth/2 -80, 0, 50, 50);
                [imagePreview addSubview: builtBanner];
            }
        });
    }

renders cells using the NSMutableArray projectContainers:

-(UICollectionViewCell*)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//    NSLog(@"cellForItemAtIndexPath");
        static NSString *identifier = @"NewCell";
        UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
        if(!reloadProjects){
            UIImageView *preview = (UIImageView*) [cell.contentView viewWithTag:[[projectIDs objectAtIndex:indexPath.row]intValue]];
            UIImageView *previewContent = [projectContainers objectAtIndex:indexPath.row];
            //    NSLog(@"fetching image tag %d", [[projectIDs objectAtIndex:indexPath.row]intValue]);

            if (!preview)
            {
                previewContent.tag = [[projectIDs objectAtIndex:indexPath.row]intValue];
                //        NSLog(@"creating previewContent %li", (long) previewContent.tag);
                [cell addSubview: previewContent];
            }

            [self.collectionView setBackgroundColor:collectionGrey];
            cell.contentView.layer.backgroundColor  = [UIColor whiteColor].CGColor;

            return cell;
        }
    return cell;
}

EDIT: Working Solution

Thanks to rob mayoff for helping me come out with a solution. This is what I ended up doing, which loads the images much faster:

// add project thumbnail images in async
        dispatch_async(imageQueue, ^{
          NSDictionary *imagePath = [currentProject objectForKey:@"image_path"];
          NSString *imageUrlString = [imagePath objectForKey: @"preview"];
          NSURL *imageUrl = [NSURL URLWithString: imageUrlString];
          NSData *imageData = [[NSData alloc] initWithContentsOfURL:(imageUrl)];
          UIImage *image = [[UIImage alloc] initWithData:(imageData)];
          if(image){
              dispatch_async(dispatch_get_main_queue(), ^{
                    NSLog(@"project with image: %@", projectTitle);
                    [imagePreview setImage: image];
              });
          }
            BOOL *builtVal = [[currentProject objectForKey:@"built"]boolValue];
            if(builtVal){
                UIImageView *builtBanner =[[UIImageView alloc]initWithImage:[UIImage imageNamed:@"built_icon.png"]];
                builtBanner.frame = CGRectMake(screenWidth/2 -80, 0, 50, 50);
                dispatch_async(dispatch_get_main_queue(), ^{
                    [imagePreview addSubview: builtBanner];
                });
            }
        });
scientiffic
  • 9,045
  • 18
  • 76
  • 149

1 Answers1

2

There are several things that code be improved in your code, but your chief complaint (“once I start scrolling, the images in the cells suddenly start appearing, but they take much longer to appear on their own”) is because you violated the commandment:

Thou shalt only access

thy view hierarchy

from the main thread.

Look at your code:

    dispatch_async(bgQueue, ^{
        ...
            [imagePreview addSubview: builtBanner];

You're manipulating the view hierarchy from a background thread. This is not allowed. For example, see the note at the bottom of this page, or the “Threading Considerations” in the UIView Class Reference.

You need to dispatch back to the main thread to update the view hierarchy.

Watch the Session 211 - Building Concurrent User Interfaces on iOS video from WWDC 2012. It talks in depth about how to do what you're trying to do, efficiently. See also this answer.

Community
  • 1
  • 1
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • thanks for your help and pointing me in the right direction. I will look into your suggestions and see if I can resolve my issue! – scientiffic Sep 07 '14 at 15:48