8

I have just created a UICollectionView in which The user can add images from their phone to the photo album feature in the app. I have the images save to the a subdirectory in the documents directory so more can be added and removed. However, when I scroll up and down the collection view, it is very laggy.

How can I make the scroll nice and smooth?

My Code: The first 16 images are preset images everything after that are from a subdirectory in documents Directory

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    CollectionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Custom" forIndexPath:indexPath];
    //Current index number
    int index=indexPath.section * noOfSection + indexPath.row;
    //Check if its the preset photos
    if(index<16){
        NSString *name=[recipePhotos objectAtIndex:indexPath.section * noOfSection + indexPath.row];
        cell.imageView.image=[UIImage imageNamed:name];
    }

//not preset photos, so retrieve the photos the user added
    else {
        NSData *data= [NSData dataWithContentsOfFile:[recipePhotos objectAtIndex:index]];
        UIImage *theImage=[UIImage imageWithData:data];

        cell.imageView.image=theImage;
        data=nil;
    }

    return cell;
}

Time Profiler gave me this

Running Time    Self        Symbol Name
568.0ms   63.1% 0.0     Main Thread  0x4048
320.0ms   35.5% 0.0     _pthread_start  0x405e
320.0ms   35.5% 0.0      thread_start
320.0ms   35.5% 0.0       _pthread_start
320.0ms   35.5% 0.0        0x1084be960
310.0ms   34.4% 1.0         0x1084be6f0
7.0ms    0.7%   0.0         mach_msg
2.0ms    0.2%   2.0         objc_msgSend
1.0ms    0.1%   1.0         -[NSAutoreleasePool release]
4.0ms    0.4%   0.0     _dispatch_mgr_thread  0x4052
4.0ms    0.4%   0.0      _dispatch_mgr_thread
4.0ms    0.4%   0.0       _dispatch_mgr_invoke
4.0ms    0.4%   4.0        kevent
3.0ms    0.3%   0.0     _dispatch_worker_thread2  0x62b24
3.0ms    0.3%   1.0      start_wqthread
3.0ms    0.3%   0.0     _dispatch_worker_thread2  0x62a84
3.0ms    0.3%   0.0      start_wqthread
3.0ms    0.3%   0.0       _pthread_wqthread
3.0ms    0.3%   0.0        _dispatch_worker_thread2
3.0ms    0.3%   0.0         _dispatch_queue_invoke
3.0ms    0.3%   0.0          _dispatch_queue_drain
3.0ms    0.3%   0.0           _dispatch_client_callout
2.0ms    0.2%   0.0            my_io_execute_passive_block
1.0ms    0.1%   0.0             __86-[NSPersistentUIManager writePublicPlistWithOpenWindowIDs:optionallyWaitingUntilDone:]_block_invoke_0835
1.0ms    0.1%   0.0              -[NSPersistentUIManager writePublicPlistData:]
1.0ms    0.1%   0.0               -[NSURL(NSURLPathUtilities) URLByAppendingPathComponent:]
1.0ms    0.1%   0.0                -[NSURL getResourceValue:forKey:error:]
1.0ms    0.1%   0.0                 CFURLCopyResourcePropertyForKey
1.0ms    0.1%   0.0             __block_global_2
1.0ms    0.1%   0.0              -[NSPersistentUIManager writeRecords:withWindowInfos:flushingStaleData:]
1.0ms    0.1%   0.0            _dispatch_call_block_and_release
1.0ms    0.1%   0.0             0x1084b8580
1.0ms    0.1%   0.0              mach_msg_send
1.0ms    0.1%   0.0               mach_msg
1.0ms    0.1%   1.0                mach_msg_trap
1.0ms    0.1%   0.0     _pthread_struct_init  0x62a83
1.0ms    0.1%   0.0      start_wqthread
1.0ms    0.1%   0.0       _pthread_wqthread
1.0ms    0.1%   1.0        _pthread_struct_init
1.0ms    0.1%   0.0     start_wqthread  0x62a7f
rog
  • 5,351
  • 5
  • 33
  • 40
Phil Scarf
  • 171
  • 1
  • 1
  • 9
  • 1
    how many images are in the view? From my experience, too many images can tend to slow the frame rate down. – LAMBORGHINI May 25 '13 at 01:46
  • 1
    It only happens with the cells loading images from the directory.. check out my code above.. i just updated it @CodeBandits – Phil Scarf May 25 '13 at 04:20
  • Do you do any custom drawing in `CollectionCell`? – rog May 25 '13 at 05:40
  • What does the time profiler in instruments tell you? – jrturton May 25 '13 at 06:56
  • Just made an edit @jrturton – Phil Scarf May 25 '13 at 17:23
  • @rog I created custom cell in that has an background image and then then the given picture for what ever index of the array. i use this code in viewdidload //Register nib UINib *nib = [UINib nibWithNibName:@"CollectionCell" bundle:nil]; [_collectionView registerNib:nib forCellWithReuseIdentifier:@"Custom"]; – Phil Scarf May 25 '13 at 17:25

4 Answers4

4

You will need to make a approach like the one you need to do in the tableviews, you will need to reuse the views, like you reuse yours cells in table view.

A really good tutorial is this one from Ray Wenderlich:

In the first part you have the basic, in the second one they talk about the reusable views, you, take a look at the link:

http://www.raywenderlich.com/22417/beginning-uicollectionview-in-ios-6-part-22

Edit

Example to load images async:

Create at your cell a method loadImageFromFile for example, that receives the path you will cal it this way:

[cell loadImageFromFile:[recipePhotos objectAtIndex:index]];

And then will look like (maybe you need to adapt something ...):

- (void) loadImageFromFile:(NSString*)path{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, NULL), ^{
        NSData *data= [NSData dataWithContentsOfFile:path];
        UIImage *theImage=[UIImage imageWithData:data];

        dispatch_async(dispatch_get_main_queue(), ^{
            cell.imageView.image=theImage;
    });
});
ggrana
  • 2,335
  • 2
  • 21
  • 31
  • You will need to load the image in background,and when loaded you will need to update the image in the cell, otherwhise your scroll will wait the image return to continue slow. – ggrana May 25 '13 at 04:32
  • Okay, whats the best approach to take care of it in the background. Would i have to seperate from the main thread? if so how @ggrana – Phil Scarf May 25 '13 at 04:42
  • I made an edit to the answer giving to you an possible approach – ggrana May 25 '13 at 04:43
  • This improved it a little bit, but its still very laggy @ggrana – Phil Scarf May 25 '13 at 05:15
  • Well, now you need to be creative, maybe your image is too big, maybe some other part of your code is making the lag, maybe you need to use a lib to do this image load, if you provide me more code maybe I can help you more – ggrana May 25 '13 at 05:47
4

@ggrana has the right idea. Loading async will definitely help. However, you're still doing redundant work if you load from file every time. One thing to consider would be augmenting the async loading with an NSCache. It's basically an NSDictionary but does memory management itself and dumps data when there is memory pressure.

So if you have the memory budget, you can actually make your thumbnails on the fly (so you don't have to hard code a size) and store them in the cache. That way your the images only pop in the first time. Every time after that, they load instantly.

You can use it like so:

@implementation ...
{
    NSCache * _johnny;  // get it?
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    [cell setImage:nil]; // since they are reused, prevents showing an old image
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         UIImage * sticker = [self thumbnailOfSize:CGSizeMake(desiredImageWidth, desiredImageHeight) 
                                          forIndex:[indexPath row]];
         dispatch_async(dispatch_get_main_queue(), ^{
             [cell setImage:sticker];
         });
    });
}

// load image from disk and cache thumbnail version
- (UIImage*) thumbnailOfSize:(CGSize)size forIndex:(NSInteger)index
{
    NSString * cacheKey = [NSString stringWithFormat:@"%@ %d", NSStringFromCGSize(size), index];
    UIImage * image = [_johnny objectForKey:cacheKey];

    if (!image) {
        image = [UIImage imageWithContentsOfFile:_imagePaths[index]];

        float desiredWidth = size.width;
        float desiredHeight = size.height;
        float actualHeight = image.size.height;
        float actualWidth = image.size.width;
        float imgRatio = actualWidth / actualHeight;
        float maxRatio = desiredWidth / desiredHeight;

        if(imgRatio != maxRatio) {
            if(imgRatio < maxRatio) {
                imgRatio = desiredHeight / actualHeight;
                actualWidth = imgRatio * actualWidth;
                actualHeight = desiredHeight;
            } else {
                imgRatio = desiredWidth / actualWidth;
                actualHeight = imgRatio * actualHeight;
                actualWidth = desiredWidth;
            }
        }

        CGRect rect = CGRectMake(0.0, 0.0, actualWidth, actualHeight);
        UIGraphicsBeginImageContextWithOptions(rect.size, FALSE, 0); // do right thing for retina
        [image drawInRect:rect];
        image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();

        // here's johnny
        [_johnny setObject:image forKey:cacheKey];
    }

    return image;
}
DanBlakemore
  • 2,306
  • 2
  • 20
  • 23
  • This approach works well. A couple of points: 1) you are missing the cell creation/dequeue line in `cellForItem...` 2) if a user scrolls to the bottom of a long collection view very quickly, the cells at the bottom might appear to cycle through a series of images as a bunch of async calls for that re-used cell finally update the image. A last-minute check of `indexPathsForVisibleCells' resolves this (basically "is this index path still visible? If not, don't update the image). – Ben Packard Jul 19 '14 at 14:22
3

So After some messing around, I've figured out that the problem was based on a few factors.

One- The images for the thumbnails were too big, so what I did was made a seperate array of images with smaller image sizes that would fit the cell. Two- With the help from @ggrana, opening a seperate thread sped up the process to and made it less laggy. Three- I also found that having an array of images rather than image locations was faster-- only problem is it takes up more memory.

Phil Scarf
  • 171
  • 1
  • 1
  • 9
  • If you do not have many images, i mean if the average user scrolls all through your collection view, they will end up using same memory at the end. so if array of images faster, go that way. – Burak SARICA Apr 30 '14 at 10:38
  • you need this http://stackoverflow.com/a/26034382/294884 and this http://stackoverflow.com/a/25604378/294884 – Fattie Apr 03 '16 at 13:46
1
fileprivate func downloadForPerformance()->void{
   for i in urlToFetchImageArray {
       var image: UIImage = dowloadImageFunc(link: i.urlString() );
       imageArray.append( image );
   }
}
fileprivate func dowloadImageFunc(link: String)->UIImage {
    let url = URL(string: link)
    let data = try? Data(contentsOf: url!)
    return UIImage(data: data!) ?? UIImage(named: "default.png")!;
}


cell?.mainImage.image = self.imageArrayPerformance[indexPath.row];

Usually the problem is slow download as each cell is dequeue.

  1. Call downloadForPerformance func before the view has appeared.
  2. This will fill an array called imageArray as a global variable
  3. Then use that array in cellForItem func