Question

Figured out slow loading images were the behind the choppy slow effect of my collectionView. I've been reading different Q&A's all day and various forum posts. It looks like the best way to solve this issue is to have the data pre-loaded available for the cellForItemAtIndexPath to be able to take what it needs.

I'm not sure how I can do this. I'm using parse as my backend, but sure if given a rough example I'd be able to figure out how to do it. From what I've seen so far I need a separate method to grab the data.

Here is the code:

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
    return 1;
}


-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return [[self objects] count];
}

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object
{
    NSArray *people = [self objects];
    static NSString *CellIdentifier = @"Cell";

    VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier: CellIdentifier forIndexPath:indexPath];


    PFObject *current;

    current = [people objectAtIndex:indexPath.item];

    PFFile *userImageFile = current[@"image"];

    [userImageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
        UIImage *image = [UIImage imageWithData:imageData];

        [[cell contentView] setContentMode: UIViewContentModeScaleAspectFit];
        [[cell imageView] setImage:image];

    }];

    [[cell title] setText:[current valueForKey:@"title"]];
    [[cell price] setText:[NSString stringWithFormat: @"£%@", [current valueForKey:@"price"]]];

    return cell;

}

So maybe the cellForItemAtIndexPath needs to call that method and take what it needs. Because the data would already be available it won't need to be loaded in the cellForItemAtIndexPath method and the cells will be populated immediately.

Please give suggestions and examples.

I was told a good way to do this would be to check for the image, if non existent provide a placeholder, if it does exist set it. Here are the changes to the above code.

Updates:

   -(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object
{

    NSArray *people = [self objects];

    static NSString *CellIdentifier = @"Cell";

    VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier: CellIdentifier forIndexPath:indexPath];



    PFObject *current;

    current = [people objectAtIndex:indexPath.item];



    PFFile *userImageFile = current[@"image"];

    [userImageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {

        if (!error)
        {

            if (!image) {
                [[cell imageView] setImage:[UIImage imageNamed:@"placeholder.png"]];


            }   else {

                image = [UIImage imageWithData:imageData];


                //resize image
                CGSize destinationSize = CGSizeMake(158,187);
                UIGraphicsBeginImageContext(destinationSize);
                [image drawInRect:CGRectMake(0,0,destinationSize.width, destinationSize.height)];

                //New image
                UIImage*newImage = UIGraphicsGetImageFromCurrentImageContext();
                UIGraphicsEndImageContext();

                //Optimise image
                NSData *imageDataCompressed = UIImageJPEGRepresentation(newImage, 0.4f);
                //  NSLog(@"Image Size %@", NSStringFromCGSize(newImage.size));//log size of image
                    NSLog(@"%@", [current valueForKey:@"title"]);


                [[cell imageView] setImage:[UIImage imageWithData:imageDataCompressed]];



            }

        }

    }];


    [[cell title] setText:[current valueForKey:@"title"]];
    [[cell price] setText:[NSString stringWithFormat: @"£%@", [current valueForKey:@"price"]]];


    return cell;

}

Place holder shows fine but remains, how do I know when the image has been loaded so I can make my cells reflect that?

Thanks for your time. Kind regards.

Update:

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object
{

    NSArray *people = [self objects];

    static NSString *CellIdentifier = @"Cell";

    VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier: CellIdentifier forIndexPath:indexPath];

    [cell.activityIndicator startAnimating];

    PFObject *current;

    current = [people objectAtIndex:indexPath.item];



    PFFile *userImageFile = current[@"image"];

    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:userImageFile.url, indexPath.item]];
    NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url                                                                   cachePolicy:NSURLRequestReturnCacheDataDontLoad
                    timeoutInterval:6.0];
                    [cell.imageView setImageWithURLRequest:urlRequest
                    placeholderImage:[UIImage imageNamed:@"placeholder.png"]
                    success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {

                    //resize image
                    CGSize destinationSize = CGSizeMake(158,187);
                    UIGraphicsBeginImageContext(destinationSize);
                    [image drawInRect:CGRectMake(0,0,destinationSize.width, destinationSize.height)];

                    //New image
                    UIImage*newImage = UIGraphicsGetImageFromCurrentImageContext();
                    UIGraphicsEndImageContext();

                    //Optimise image
                    NSData *imageDataCompressed = UIImageJPEGRepresentation(newImage, 0.4f);

                    cell.imageView.image = [UIImage imageWithData:imageDataCompressed];
                    NSLog(@"Image Size %@", NSStringFromCGSize(newImage.size));//log size of image
                   [cell.activityIndicator stopAnimating];

                    } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError          *error) {
                    NSLog(@"Failed to download image: %@", error);
                                        }];




    return cell;

}

Latest Update:

Set up a method that gets data from parse.com and stores in an NSMutableDictionary then in a mutable array. I store the title, price and URL to image of the garment.

- (void)grabDataFromCloud
{

    self.model = [NSMutableArray array];
    for (PFObject *object in [self objects]) {
        PFFile *imageFile = [object valueForKey:@"image"];
        NSURL *url = [NSURL URLWithString:imageFile.url];

        NSMutableDictionary *newObject = [NSMutableDictionary dictionaryWithDictionary:@{@"title": [object valueForKey:@"title"], @"price": [object valueForKey:@"price"], @"imageUrl": url}];
        [[self model] addObject:newObject];
    }
} 

This gets called in my cellForItemsAtIndexPath method.

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object
{

    [self grabDataFromCloud];
    static NSString *CellIdentifier = @"Cell";

    VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier: CellIdentifier forIndexPath:indexPath];

    [cell.activityIndicator setHidden:YES];

    NSMutableDictionary* d = [self.model objectAtIndex:indexPath.item];
    cell.title.text = d[@"title"];
    cell.price.text = [NSString stringWithFormat:@"£%@", d[@"price"]];

    if (d[@"image"]) {
         cell.imageView.image = d[@"image"];
    } else { // if not, download it
        cell.imageView.image = nil;

        dispatch_queue_t backgroundQueue = dispatch_queue_create("test", 0);

        dispatch_async(backgroundQueue, ^{

             NSData* data = [NSData dataWithContentsOfURL:d[@"imageUrl"]];
             UIImage* img = [UIImage imageWithData:data];
             d[@"image"] = img;
                 dispatch_async(dispatch_get_main_queue(), ^{
                    //causes crash Assertion failure in -[UICollectionView _endItemAnimations],
                    // /SourceCache/UIKit/UIKit-2935.137/UICollectionView.m:3687
                     //   [self.collectionView reloadItemsAtIndexPaths:@[indexPath]];
                 });
        });

    }

        return cell;
    }
Was it helpful?

Solution 4

After several days the issue was my images were far too large. I had to resize them and this instantly solved my issue.

I literally narrowed things down and checked my images to find they were not being resized by the method I thought was resizing them. This is why I need to get myself used to testing.

I learnt a lot about GCD and caching in the past few days but this issue could have been solved much earlier.

OTHER TIPS

I'd suggest you to use AFNetworking's UIImageView+AFNetworking category. It will handle the placeholder etc automatically, and will do everything in a background thread, ensuring that the main thread doesn't get blocked. Specifically, this is the method you'd want to call:

- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholderImage;

It is up to you to supply a placeholder image (or nil) when the image is first needed and to start downloading it, and then to hang on to the image once it has been downloaded so that ever after that you can supply it instantly. This example is for a table view, but the principle is exactly the same; the key thing is that my data model is a bunch of NSMutableDictionary objects, and each dictionary in not only the url for the picture we are supposed to have but also a place for keeping the image once it has been downloaded:

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    NSMutableDictionary* d = (self.model)[indexPath.row];
    cell.textLabel.text = d[@"text"];
    if (d[@"im"]) { // if we have a picture, supply it
        cell.imageView.image = d[@"im"];
    } else if (!d[@"task"]) { // if not, download it
        cell.imageView.image = nil;
        NSURLSessionTask* task = [self.downloader download:d[@"picurl"]
                                         completionHandler:^(NSURL* url){
            if (!url)
                return;
            NSData* data = [NSData dataWithContentsOfURL:url];
            UIImage* im = [UIImage imageWithData:data];
            d[@"im"] = im;
            dispatch_async(dispatch_get_main_queue(), ^{
                [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
            });
        }];
    }
    return cell;
}

I suggest a different approach.

Google for - or use the search on SO - Asynchrnous loading. Nearly every app programmer faces this issue earlier or later. Consequentially there are tons of tutorials out there.

This is one of them. http://www.markj.net/iphone-asynchronous-table-image/ I think it is older than the UICollectionView and therfore explains it for UITableView. Both data source delegates are so close to each other that you can easily adopt the solution to your collection.

There are smarter ways of acomplishing your goal. But I think tht this way is a good starting point. You may later want to refactor the solution once you got comforatble with the approach in general.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top