Efficiently handling UIImage loading in UICollectionView - xcode

In my app the user can take a photo of their pet, or whatever, and that will show in the collection view as a circular picture. Using cellForItemAtIndexPath to check for the existence of a user generated photo (which is stored in the Documents directory as a png file), if not display a default photo.
Issues:
- When Scrolling the UICollectionView the image load halts the
smoothness of the scroll UI as the picture is going to appear on
screen.
- If you scroll the picture off the screen many times the
app crashes due to memory pressure.
QUESTION: Is there an efficient way of loading and keeping the images in memory without having to keep loading the image after the first time it is displayed?
I thought rasterizing the images would help, but it seems the UI scrolling and memory issues persist.
Here is the code:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifierPortrait = #"allPetCellsPortrait";
AllPetsCell *cell = (AllPetsCell *)[collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifierPortrait forIndexPath:indexPath];
CGRect rect = CGRectMake(cell.petView.center.x, cell.petView.center.y, cell.petView.frame.size.width, cell.petView.frame.size.height);
UIImageView *iv = [[UIImageView alloc] initWithFrame:rect];
//set real image here
NSString *petNameImage = [NSString stringWithFormat:#"%#.png", self.petNames[indexPath.section][indexPath.item]];
NSString *filePath = [_ad documentsPathForFileName:petNameImage];
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:filePath];
iv.image = nil;
if (fileExists){
[_allPetsCollectionView performBatchUpdates:^{
if (iv && fileExists) {
iv.image = [UIImage imageWithContentsOfFile:filePath];
}
} completion:^(BOOL finished) {
//after
}];
//loadedImage = YES;
[collectionView layoutIfNeeded];
}//file exists
else{
iv.image = [UIImage imageNamed:[NSString stringWithFormat:#"%#", self.contents[indexPath.section][indexPath.item]]];
[cell.petNameLabel setTextColor:[UIColor darkGrayColor]];//be aware of color over pictures
shadow.shadowColor = [UIColor whiteColor];
}
cell.petView.clipsToBounds = YES;
UIColor *color = MediumPastelGreenColor;
if ([[NSString stringWithFormat:#"%#", self.contents[indexPath.section][indexPath.item]] isEqualToString:#"addpet.png"]) {
color = MediumLightSkyBlueColor;
}
[cell.petView setBounds:rect forBorderColor:color];
[cell.petView addSubview:iv];
if (![[NSString stringWithFormat:#"%#", self.contents[indexPath.section][indexPath.item]] isEqualToString:#"addpet.png"])
[iv addSubview:cell.petNameLabel];
//TODO: optimize the cell by rasterizing it?
if ([_contents count] > 4) {
cell.layer.shouldRasterize = YES;
}
return cell;
}
Here is a screen shot:
Thanks!

Assigning an image to the collection view cell's imageView doesn't cost you any performance issues at all..
There are 2 problems here.
You are fetching the contents (datasource for the collection view) from within the collection view's delegate method. This is causing the delay.
You are adding the imageView holding the image again and again. Thus, each time the cell is displayed, a new imageView holding a newly fetched data is being added as the subView to the reused cell (As the new imageView covers the old one as both have the same frame, you wouldn't notice. And at the sametime this eats up memory). This is causing the memory pressure crash.
SOLUTION
FOR IMPROVING PERFORMANCE
Fetch the image contents from your model class(if you are using one), or from you viewController's viewDidLoad() (You should never fetch datasource from within the collection view's delegate method). Perform the fetching process in the background queue as it wouldn't affect your UI in any manner.
Store the contents onto an array and fetch the contents only from the array to populate your collection view cells.
FOR AVOIDING MEMORY CRASH
As long as you are not using a custom collection view cell, its better to remove all the subviews from the collectionViewCell's content view each time the cell is dequeued.
I hope this solves your problem :)

I recently discovered NSCache which is a dictionary type object that I can store my *already loaded image*s into and check to see if they are loaded with the -cellForItemAtIndexPath method.
I changed the above code to accommodate for this also by using a custom dispatch queue that would be sent to the main queue. This still causes a slight pause bump in the scrolling when the initial image is loaded but after that it is read from the image cache. Not totally optimal yet but I think I am headed in the right direction.
here is the code:
if (fileExists){
//DONE: implement NSCACHE and dispatch queue
const char *petImageQ = "pet image";
dispatch_queue_t petImageQueue = dispatch_queue_create( petImageQ, NULL);
//check to see if image exsists in cache - if not add it.
if ([_petImagesCache objectForKey:petNameImage] == nil) {
dispatch_async(petImageQueue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
[collectionView performBatchUpdates:^{
iv.image = [UIImage imageWithContentsOfFile:filePath];
} completion:^(BOOL finished) {
//add to cache
[_petImagesCache setObject:iv.image forKey:petNameImage];
}];
});
});
}
else{
//load from cache
iv.image = [_petImagesCache objectForKey:petNameImage];
}
I hope this helps. Please add more to this for improving it!

Related

Programmatically created UICollectionView: cells overlap after scrolling

I'm trying to implement a custom UICollectionView that shows all elements from the asset library. While displaying the items is not that big of a deal, I run into a weird bug which I think has to do something with the layout I'm using.
Let's start with some screenshots to indicate what's going on. I'll just post the links to these images to be easy on your scroll wheels :-)
1. Initial
https://dl.dropboxusercontent.com/u/607872/stackoverflow/01-uicollectionview-start.png
Everything ok.
2. Scrolled down
https://dl.dropboxusercontent.com/u/607872/stackoverflow/02-uicollectionview-scrolleddown.png
The images from the first row (which isn't visible anymore) repeat behind the images in the last row. The first one is invisible because it is entirely overlapped.
3. Scrolled up again
https://dl.dropboxusercontent.com/u/607872/stackoverflow/03-uicollectionview-scrollup.png
The first image (in the background) is from the same row, but should be the third. The 2nd and 3rd image are from the last row.
Here's some code. I hope I didn't miss any relevant stuff - if so, please let me know.
MultiImagePickerController.m (:UICollectionViewController, protocols: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout)
- (void)loadView {
[...] // layout stuff
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
[layout setScrollDirection:UICollectionViewScrollDirectionVertical];
_collectionView = [[UICollectionView alloc] initWithFrame:collectionRect collectionViewLayout:layout];
[_collectionView setDataSource:self];
[_collectionView setDelegate:self];
[_collectionView registerClass:[MultiImagePickerCell class] forCellWithReuseIdentifier:#"MultiImagePickerCellIdentifier"];
[_collectionView setBackgroundColor:[UIColor whiteColor]];
[self.view addSubview:_collectionView];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MultiImagePickerCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"MultiImagePickerCellIdentifier" forIndexPath:indexPath];
if ( !cell ) {
CGSize sizeCell = [MultiImagePickerCell defaultSize];
cell = [[MultiImagePickerCell alloc] initWithFrame:CGRectMake( 0, 0, sizeCell.width, sizeCell.height )];
}
NSString *groupname = [self getKeyFromMutableDictionary:assetList forIndex:indexPath.section];
[cell addImageUsingAsset:[[assetList objectForKey:groupname] objectAtIndex:indexPath.row]];
return cell;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
CGSize sizeCell = [MultiImagePickerCell defaultSize];
return CGSizeMake( sizeCell.width + 10.0, sizeCell.height + 10.0 );
}
MultiImagePickerCell - (void)addImageUsingAsset:(ALAsset *)asset;
This method adds a UIImageView and a UIImage to the cell to show the asset. Nothing special, in my opinion. When I set breakpoints in cellForItemAtIndexPath, I can see that the correct asset is being read from the assetList and the views are calculated correctly. The drawRect method of the cell is NOT implemented.
I think I experienced the same problem a few years back when I tried to programmatically create a UITableViewController, but I can't remember how I solved it.
Any suggestions?
Got it - I had to remove the subviews from my cells.

UICollectionView poor scrolling performance with simple UICollectionViewCell

I'm using UICollectionView to show grid based View in iPad. I have only 200 (20rows x 10 columns) cells at a time on screen, each cell contain one UILabel only. It starts to get slower on scrolling and not smooth like in TableView.
I don't know the reason for this slower scrolling performance. How do I improve?
Thanks in advance!
I have 3 collectionviews, but the main content uicollectionview is the one which has been tagged 2. Here is my code
if (collectionView.tag==0) { //This tagged collection view is for Left side selection box
LeftSelectionBoxItem *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"SelectionCell" forIndexPath:indexPath];
return cell;
}
else if (collectionView.tag==1){ //This tagged collection view is for to show column names
TopColumnItem *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"ColumnCell" forIndexPath:indexPath];
cell.columnName.text = [NSString stringWithFormat:#"%#",[self.columnNames objectAtIndex:indexPath.item]];
return cell;
}
else if (collectionView.tag==2) { //This tagged collection view is for to show main content
GridItem *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"GridCell" forIndexPath:indexPath];
cell.dataLabel.text = [NSString stringWithFormat:#"%#",[[self.dataModel objectAtIndex:indexPath.section] objectForKey:[self.columnNames objectAtIndex:indexPath.item]]];
return cell;
}
else {
return nil;
}
Having a lot of UILabels is sometimes expensive. Try changing them to CATextLayers instead, as these have much less overhead. Note that if you do this and use rasterizing, you will need to tell the layer to rasterize for retina display devices too.

Custom UITableViewCell with large UIImageView causing scroll lag

I have a custom UITableViewCell that consists of a UIImageView and a UILabel. The cell is 320x104px and the imageView takes up the whole area with the label in front. There are only 8 cells.
in ViewDidLoad I am creating all needed images up front and caching them in a dictionary at the correct dimensions.
When I scroll the UITableView there is a noticable lag every time a new cell is encountered. This makes no sense to me as the image it is using is already created and cached. All that I'm asking of the cell is for its UIImageView to render the image.
I am using a custom cell with its view in a xib and configuring my UITableView to use it with:
[self.tableView registerNib:[UINib nibWithNibName:#"ActsCell" bundle:nil]
forCellReuseIdentifier:myIdentifier];
Cell creation and configuration:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString* reuseIdentifier = #"ActsCell";
ActsCell* cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier];
// Configure the cell...
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
- (void)configureCell:(ActsCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
Act* act = [self.acts objectAtIndex:indexPath.row];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.title.text = act.name;
cell.imageView.image = [self.imageCache objectForKey:act.uid];
}
What could be causing the lag? There would seem to be no benefit in trying to do anything Async as all the time-intensive work is done.
Do you load images from local files by any chance?
By using Instruments I've found that there is some lazy loading mechanism in UIImage - the real image data was decompressed from PNG only at the stage of rendering it on the main thread which caused the lag during scrolling.
So, after the loading of UIImage with -initWithContentsOfFile: method, I've added code to render the contents of this image to offscreen context, saved that context as new UIImage and used it for UIImageView in UITableViewCell, this made the scroll to be smooth and pleasant to the eye again.
In case of reference there is the simple code I'm using to force reading of image contents in separate thread (using ARC):
UIImage *productImage = [[UIImage alloc] initWithContentsOfFile:path];
CGSize imageSize = productImage.size;
UIGraphicsBeginImageContext(imageSize);
[productImage drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)];
productImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
And I think the surface of UIImage created in such a way will be in the supported format suited for rendering, which will also offload work needed to render it on main thread.
EDIT: The docs for UIGraphicsGetImageFromCurrentImageContext() say that it should be only used from main thread, but searching on the net or SO shows that starting from iOS 4 the UIGraphics.. methods became thread safe.

paging (horizontal swiping) through multiple uitableviews

I am trying to put a number tables in uiscrollview such that it can swipe through each (paging). This is done on nicely on TweetDeck.
I'm using https://github.com/andreyvit/SoloComponents-iOS/tree/master/ATPagingView
However im struggling to figure out how to do it without leaking memory.
- (UIView *)viewForPageInPagingView:(ATPagingView *)pagingView atIndex:(NSInteger)index {
myTableViewController *t = [[servicesController alloc] init];
t.delegate = self.navigationController; //such that the tableview can call nav controller
t.info = [array objectAtIndex:index]; //Data that is different for every page
return [t.view autorelease];
}
Does anyone have any suggestions as to how I should modify ATPagingView to accomodate for UIViewControllers instead of UIViews? Or any other examples?

How can I make NSCollectionView finish drawing before the view containing it is made visible?

I'm trying to clean up the UI on my application (built for 10.5) and one thing that's rather annoying is that when I swap to the library I reload the contents and the nscollectionview that displays the items fades the old content out before fading the new ones in. I don't mind the fade in/out of old/new items, but the way i've programmed the view swapping should make the library reload happen before the the nsview that contains the nscollectionview is inserted into the main window.
Anyone had this sort of problem before?
Here's an abridged version of how the swap between different parts of the application to the library goes:
In the main controller:
-(void)setMainPaneToLibrary {
if(viewState == VIEW_STATE_LIBRARY)
return;
[libraryController refreshDevices:self];
[libraryController refreshLibraryContents];
[menuController setSelectionToIndex:0];
viewState = VIEW_STATE_LIBRARY;
[self removeSubview]; //clear the area
[mainPane addSubview:[libraryController view]];
}
In the library controller:
-(void)refreshLibraryContents {
[self loadLibraryContents:[rootDir stringByAppendingPathComponent:currentDir]];
}
-(void)loadLibraryContents:(NSString *)folderToLoad {
int i;
int currentSelection = [libraryArrayController selectionIndex]; //used to preserve selection and smooth folder changing
[libraryArrayController setContent:nil];
//expand the path to load
folderToLoad = [folderToLoad stringByExpandingTildeInPath];
NSFileManager * fileManager = [NSFileManager defaultManager];
//load the files in the folder
NSArray * contents = [fileManager directoryContentsAtPath:folderToLoad];
//create two seperate arrays for actual files and folders - allows us to add them in order
NSMutableArray * directories = [[NSMutableArray alloc] init];
NSMutableArray * musicFiles = [[NSMutableArray alloc] init];
//... a load of stuff filling the file arrays ...
for(i=0;i<[directories count];i++) {
[libraryArrayController addObject:[directories objectAtIndex:i]];
}
for(i=0;i<[musicFiles count];i++) {
[libraryArrayController addObject:[musicFiles objectAtIndex:i]];
}
if(currentSelection >= 0) {
[libraryArrayController setSelectionIndex:currentSelection];
}
[directories release];
[musicFiles release];
}
The issue is not that the collection view hasn't finished drawing, it's that it animates the adding and removal of the subviews. It may be possible to create a transaction that disables layer actions while you're reloading the collection view's content. Listing 2 (temporarily disabling a layer's actions) shows how to do this. I haven't tested this, so I don't know if this will do what you want, but this is where I'd start.

Resources