1

I have a UICollectionView which shows some data from CoreData entity. This entity has a lot of attributes and one of them is image (transformable type).

My UICollectionView scrolling is not smooth. I think the reason is a big pictures in image attribute (from 300 to 500k).

Here is the code of my cellForItemAtIndexPath:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    AppDelegate *d = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    static NSString *CellIdentifier = @"cellRecipe";

    RecipeCell *cell = (RecipeCell *)[self.collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];

    cell.image.layer.borderColor = [UIColor blackColor].CGColor;
    cell.image.layer.borderWidth = 1.0f;

    Recipes *recipe = [recipesArray_ objectAtIndex:indexPath.item];

    if ([recipe.isPaid integerValue] == 0)
        [cell.freeImage setHidden:NO];
    else [cell.freeImage setHidden:YES];

    NSObject *object = [[NSUserDefaults standardUserDefaults] objectForKey:@"com.________.fullhomechef"];
    if ([object isEqual:[NSNumber numberWithBool:YES]]) {
        [cell.freeImage setHidden:YES];
    }

    if ([recipe.isFavorite integerValue] == 0) 
        [cell.favImage setImage:[UIImage imageNamed:@"toFavorite.png"]];
    else [cell.favImage setImage: [UIImage imageNamed:@"toFavorite_.png"]];

    if ([recipe.isFitness integerValue] == 1)
        [cell.fitImage setHidden:NO];
    else [cell.fitImage setHidden:YES];

    if ([d.currLang isEqualToString:@"ru"]) 
        [cell.recipeName setText: recipe.nameru];
    else [cell.recipeName setText: recipe.nameen];

    [cell.image setImage:recipe.image];

    int difficulty = [recipe.difficulty intValue];
    [cell.difficultyImage setImage: [mainTabController imageForRating:difficulty]];

    return cell;
}

I've tried to comment line [cell.image setImage:recipe.image]; but scrolling is still
intermittent.

What's wrong am I do? May be there is some method how to work with big images? Cuz in previous versions I have used images with size 50-70k.

P.S. If I patiently scroll it down to the end, scrolling become more smooth.

After the answer of Guto Araujo

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"cellRecipe";

    RecipeCell *cell = (RecipeCell *)[self.collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];

    cell.image.layer.borderColor = [UIColor blackColor].CGColor;
    cell.image.layer.borderWidth = 1.0f;

    Recipes *recipe = [fetchRecipes objectAtIndexPath:indexPath];

    if ([recipe.isPaid integerValue] == 0) {
        [cell.freeImage setHidden:NO];
    }
    else [cell.freeImage setHidden:YES];

    if (wasPaid == YES) {
        [cell.freeImage setHidden:YES];
    }

    if ([recipe.isFavorite integerValue] == 0) 
        [cell.favImage setImage:[UIImage imageNamed:@"toFavorite.png"] ];
    else [cell.favImage setImage: [UIImage imageNamed:@"toFavorite_.png"]];

    if ([recipe.isFitness integerValue] == 1)
        [cell.fitImage setHidden:NO];
    else [cell.fitImage setHidden:YES];

    if ([d.currLang isEqualToString:@"ru"]) {
        [cell.recipeName setText: recipe.nameru];
    } else [cell.recipeName setText:recipe.nameen];

    __weak typeof(self) weakSelf = self;
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        UIImage *image = recipe.image;

        dispatch_async(dispatch_get_main_queue(), ^{
            // Set via the main queue if the cell is still visible
            if ([weakSelf.collectionView.indexPathsForVisibleItems containsObject:indexPath]) {
                RecipeCell *cell =
                (RecipeCell *)[weakSelf.collectionView cellForItemAtIndexPath:indexPath];
                cell.image.image = image;
            }
        });
    }];
    [thumbnailQueue addOperation:operation];

    int difficulty = [recipe.difficulty intValue];
    [cell.difficultyImage setImage: [mainTabController imageForRating:difficulty]];

    return cell;
}
Romowski
  • 1,518
  • 5
  • 25
  • 50
  • 1
    Why do alloc a new `MainTabController` every time? – Marcelo Nov 18 '13 at 04:27
  • @MarceloFabri this is simple function which returns a small image depending on **difficulty level** from 1 to 5, and I have already initialized it in **viewDidLoad** – Romowski Nov 18 '13 at 04:32
  • Make sure the image size is same as your cell of a collection view. If the size of an image is bigger than the size of the cell of collection view, then the scrolling will not smooth. – Manthan Nov 18 '13 at 05:46
  • @Manthan It's bigger.. Am I need to have two images (small thumb for UICollectionView & big)? – Romowski Nov 18 '13 at 05:50
  • @Romowski: Yes, try putting the image size same as your cell and you will find the scrolling better. – Manthan Nov 18 '13 at 05:57
  • @Manthan what can you say about "I've tried to comment line [cell.image setImage:recipe.image]; but scrolling is still intermittent." ? may be the reason is not in image – Romowski Nov 18 '13 at 06:04
  • 1
    So you store the pictures in the database itself? If so don't do that ;) just store a path in your db and store the images in a folder. – HAS Nov 18 '13 at 06:16
  • @HAS thanks I'll try! can I store small thumbnails in database but the main images in folder? – Romowski Nov 18 '13 at 06:18
  • 1
    You can try and measure performance ;) It's just not what a database is made for ;). You should even be able to make a folder structure in which so save the images with paths and names so that you wouldn't need a path attribute (e.g. if you choose the recipe name you can dynamically load it like `[UIImage imageNamed:[[NSString stringWithFormat:@"%@", recipe.name]]];`) sorry for any syntax errors but I'm on mobile … – HAS Nov 18 '13 at 06:32
  • 1
    @HAS Thank you very much! I'll try and write you result! – Romowski Nov 18 '13 at 06:36
  • @HAS Thank you!!! Great!!! Now scrolling is smooth and fast )))) – Romowski Nov 19 '13 at 01:47
  • 1
    Romowski, happy to hear it worked! @HAS It's a good debate re: storage. As I understand it, Core Data actually stores images outside of the database with that option selected. I guess the drawback is that it's a leap of faith vs managing it directly. In any case, I think both points are fair. Thanks for the good debate! – Guto Araujo Nov 19 '13 at 23:26

2 Answers2

4

One approach would be to use an operation queue to load the images in the background as described in this UICollectionView tutorial by Bryan Hansen, specifically steps #45-47:

Add a property for the Operation Queue:

@property (nonatomic, strong) NSOperationQueue *thumbnailQueue;

Initialize and configure maxConcurrentOperationCount in viewDidLoad:

self.thumbnailQueue = [[NSOperationQueue alloc] init];
self.thumbnailQueue.maxConcurrentOperationCount = 3; // Try different values for this variable

Use the Operation Queue in collectionView:cellForItemAtIndexPath: to load images from Core Data asynchronously:

// Change [cell.image setImage:recipe.image] for the code below

__weak typeof(self) weakSelf = self;
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    UIImage *image = recipe.image;

    dispatch_async(dispatch_get_main_queue(), ^{
        // Set cell.image via the main queue if the cell is still visible
        if ([weakSelf.collectionView.indexPathsForVisibleItems containsObject:indexPath]) {
            RecipeCell *cell =
                (RecipeCell *)[weakSelf.collectionView cellForItemAtIndexPath:indexPath];
            cell.image = image;
        }
    });
}];

[self.thumbnailQueue addOperation:operation];

Update 1:

In addition, you could do a few things to optimize image loading:

  1. Create a thumbnail attribute in the Recipe entity to store a smaller version of the image to be used in the Collection View cells. Make that image no larger than the max width and height of the RecipeCell. Then, use that attribute in the block above:

     UIImage *image = recipe.thumbnail;
    
  2. Use external storage for the main images. I'd suggest you try the option "Store In External Record File" from the image attribute:

    Store In External Record File

    Using this option will let Core Data manage external file storage for you. Alternatively, you could store just the image path and store the files on disk as suggested by @HAS. Here is a question discussing these options: Storing UIImage in Core Data with the new External Storage flag

  3. If you use the "Store In External Record File" option, consider moving the main image to a separate Photo Entity with a relationship to Recipe: Recipe <-> Photo . In such case, image attribute would be in the Photo entity. This is so that they are not loaded into memory until the relationship is followed.

    There is additional discussion on best ways to store Images in Core Data here: How to store an image in core data

    Update 2:

  4. If scrolling still isn't smooth, consider using NSCache as suggested by @DavidH. There is an example implementation here: Caching an Image and UICollectionView . In fact, it seems the right thing to do even if scrolling is already working smoothly.

Hope this helps.

Community
  • 1
  • 1
Guto Araujo
  • 3,824
  • 2
  • 21
  • 26
1

There is a lot going on here, but let me give you a high level idea of one way to make this work better.

First, if you can, prefetch the first set of images needed for the collectionView, so you have them on hand the first time that view first shows.

Once you get those, you can try to start fetching more in the background, or optionally when the collection view has finished displaying the first set (you have to experiment here).

The idea is that once you have something to show the user, you attempt to prefetch the "hard to get" items immediately, and cache them, hoping you can get them faster than the user can scroll.

Once you get something in the background, put it into a NSCache, and in your routine that vends cells, look for the image in the NSCache, if there apply it to the cell. If its not there then just make the background of the UIImageView gray (or some color).

If the user is scrolling faster than you can retrieve images, you can in a method receiving images from the background (and on the main thread) look at the current visible cells, and if any are missing an image you just retrieved, apply it to the cell.

If the user scrolls slowly, you will probably stay ahead of the curve. If they scroll really fast, they are going to see empty image views, but those views will get filled in when they scroll backwards.

Using the NSCache works really well, since the system will give you lots of memory for it if available, and if it needs the memory back, it just removes items from your cache, requiring you to go fetch them again.

David H
  • 40,852
  • 12
  • 92
  • 138
  • So you would store the images in the database? I still think that it is better to save URLs (please see [this link](https://developer.apple.com/library/ios/documentation/cocoa/Conceptual/CoreData/Articles/cdPerformance.html) quite at the end it says `It is better, however, if you are able to store BLOBs as resources on the filesystem, and to maintain links (such as URLs or paths) to those resources. You can then load a BLOB as and when necessary.` what do you think? Wouldn't that be most efficient (and most performant) way to deal with images? – HAS Nov 18 '13 at 18:24
  • 1
    @HAS Sorry to have missed this. What I did was only save small thumbnails in the repository, for larger issues they were stored on disk and the repository just had a filePath to the image. The method I described is the same for both techniques. Also, I enforced a strict ordering, so images lower in the tableView were not shown until the preceding one was. It looked really nice and finished, but it was a lot of work to get right (and I might not do it that way again!) – David H Nov 18 '13 at 18:32
  • @DavidH I added an update re: NSCache based on your answer because that seems a pretty important point that I missed and it will be helpful to others. Hope that's fine with you. Thanks for that and for all the other insights (e.g. prefetching). I'll try that next. – Guto Araujo Nov 20 '13 at 00:00
  • @GutoAraujo no problem - its all about getting to good solutions. – David H Nov 20 '13 at 00:01