How do I correctly set up asynchronous image downloading within a UICollectionView that uses a custom cell?

StackOverflow https://stackoverflow.com/questions/22946002

سؤال

At this point I'm really fed up. It's been nearly a week now trying to solve this issue so I can move ahead. I've read multiple threads and done multiple searches in regards to my slow loading choppy UICollectionView.

I've tried to do this without any libraries as well as with SDWebImage and AFNetwork. It still doesn't fix things. Images loading isn't really a problem. The problem arrives when I scroll to cells that aren't currently showing on the screen.

As of now I've deleted all the code and all traces of any libraries and would like to get help in order to implement this properly. I've made about 2 posts on this already and this would be my third attempt coming from a different angle.

Information

My backend data is stored on Parse.com I have access to currently loaded objects by calling [self objects] My cellForItemAtIndex is a modified version that also returns the current object of an index.

From what I understand in my cellForItemAtIndex I need to check for an image, if there isn't one I need to download one on background thread and set it so it shows in the cell, then store a copy of it in cache so that if the associated cell goes off screen when I do scroll back to it I can use the cached image rather than downloading it again.

My custom parse collectionViewController gives me all the boiler plate code I need to get access to next set of objects, current loaded objects, pagination, pull to refresh etc. I really just need to get this collection view sorted. I never needed to do any of this with my tableview of a previous app which had much more images. It's really frustrating spending a whole day trying to solve an issue and getting no where.

This is my current collectionView cellForItemAtIndex:

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

    static NSString *CellIdentifier = @"Cell";
    VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier: CellIdentifier forIndexPath:indexPath];

 // check for image
 // if there is a cached one use that
 // if not then download one on background thread
 // set my cells image view with that image
 // cache image for re-use.

    // PFFile *userImageFile = object[@"image"];
    [[cell title] setText:[object valueForKey:@"title"]]; //title set
    [[cell price] setText:[NSString stringWithFormat: @"£%@", [object valueForKey:@"price"]]]; //price set

    return cell;

}

I am also using a custom collectionViewCell:

@interface VAGGarmentCell : UICollectionViewCell
@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@property (weak, nonatomic) IBOutlet UITextView *title;
@property (weak, nonatomic) IBOutlet UILabel *price;
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;

@end

If there's any more information you'd like please ask. I'd just like a clear example in code of how to do this correctly, if it still doesn't work for me then I guess there is something wrong some where within my code.

I'm going to continue reading through various threads and resources I've come across in the last few days. I can say one benefit in this experience is that I have a better understanding of threads and lazy loading but it is still very frustrated that I have made any progress with my actual app.

Incase you wondered here is my previous post: In a UICollectionView how can I preload data outside of the cellForItemAtIndexPath to use within it?

I'd either like to do this quick and manually or using the AFNetwork as that didn't cause any errors or need hacks like SDWebImage did.

Hope you can help

Kind regards.

هل كانت مفيدة؟

المحلول 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.

نصائح أخرى

You can make use of the internal cache used by NSURLConnection for this.

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"VAGGarmentCell" forIndexPath:indexPath];

    //Standard code for initialisation.
    NSURL *url; //The image URL goes here.
        NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:5.0]; //timeout can be adjusted

        [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
         {
             if (!connectionError)
             {
                 UIImage *image = [UIImage imageWithData:data];

                 //Add image as subview here.
             }
         }];

    .
    .
    return cell;

}

This is for a table view, but same concept basically. I had the same issue you were having. I had to check for a cached image, if not, retrieve it from a server. The main thing to watch out for is when you retrieve the image back, you have to update it in the collection view on the main thread. You also want to check if the cell is still visible on the screen. Here is my code as an example. teamMember is a dictionary and @"avatar" is the key which contains the URL of the user's image. TeamCommitsCell is my custom cell.

// if  user has an avatar
    if (![teamMember[@"avatar"] isEqualToString:@""]) {
        // check for cached image, use if it exists
        UIImage *cachedImage = [self.imageCache objectForKey:teamMember[@"avatar"]];
        if (cachedImage) {
            cell.memberImage.image = cachedImage;
        }
        //else  retrieve the image from server
        else {
            NSURL *imageURL = [NSURL URLWithString:teamMember[@"avatar"]];

            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
                NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
                // if valid data, create UIImage
                if (imageData) {
                    UIImage *image = [UIImage imageWithData:imageData];
                    // if valid image, update in tableview asynch
                    if (image) {
                        dispatch_async(dispatch_get_main_queue(), ^{
                            TeamCommitsCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath];
                            // if valid cell, display image and add to cache
                            if (updateCell) {
                                updateCell.memberImage.image = image;
                                [self.imageCache setObject:image forKey:teamMember[@"avatar"]];
                            }
                        });
                    }
                }
            });
        }
    }

NSURLCache is iOS's solution to caching retrieved data, including images. In your AppDelegate, initialize the shared cache via:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:8 * 1024 * 1024
                                                      diskCapacity:20 * 1024 * 1024
                                                          diskPath:nil];
    [NSURLCache setSharedURLCache:cache];
    return YES;
}

-(void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
    [[NSURLCache sharedURLCache] removeAllCachedResponses];
}

Then use AFNetworking's UIImageView category to set the image using:

[imageView setImageWithURL:myImagesURL placeholderImage:nil];

This has proven to load images the second time around incredibly faster. If you are worried about loading images faster for the first time, you will have to create a way to determine when and how many images you want to load ahead of time. It is very common to load data using paging. If you are using paging and still are having trouble, consider using AFNetworking's:

- (void)setImageWithURLRequest:(NSURLRequest *)urlRequest
              placeholderImage:(UIImage *)placeholderImage
                       success:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image))success
                       failure:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error))failure;

This way you can create an array of UIImages and using this method to return the images for each cell before dequeuing the cell. So in this case you would have two parallel arrays; one holding your data and the other holding corresponding UIImages. Memory management will eventually get out of hand so keep that in mind. If someone scrolls quickly to the bottom of the available cells, there is honestly not much else you can do since the data depends on the network connection of the user.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top