I get didreceivememorywarning uitableview usuing AFNetworking to download the images? - iphone

Hello everyone I have a UITableView which I use AfNetworking to load very large images. But however when the didreceivememorywarning is hit my app just crashes. Here is my code :
In AFNetworking+uiimageview.m I have added this as suggested :
#implementation AFImageCache
- (id)init
{
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(didReceiveMemoryWarning)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
}
return self;
}
- (void)didReceiveMemoryWarning
{
DebugLog(#"AFNetworking DID RECEIVED MEMORY WARNING ");
[self removeAllObjects];
}
And my code in uitableview is as follows :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
TousCell *tvCell;
NSArray *nib;
nib = [[NSBundle mainBundle] loadNibNamed:#"TousCell" owner:self options:nil];
tvCell = [nib objectAtIndex:0];
tvCell.lbltitle.text=[[dict valueForKey:#"message"]objectAtIndex:i];
[tvCell.imgPhoto setImageWithURL:[NSURL URLWithString: [NSString stringWithFormat:#"http://test.com/%#", [[dict valueForKey:#"photo"]objectAtIndex:i]]]];
May be I am recreating the cells each and everytime? But I am sure its linked to the images download as the images are v large sometimes ( in size about 2 - 3 Mb?).

You're recreating the table cell each time so if images are different they will take a lot of space. You must reuse table view cells as much as possible using the
[tableView dequeReusableCellWithIdentifier:]
method and registering your nib in the view controller view did load using the:
[tableView registerNib:forCellReuseIdentifier:]
method.
Also note that if the images are large, AFNetworking offers you the possibility to downsize them before returning to the caller. This is especially useful if you need to fit a large image on a small cell (e.g. the one from a table view). Note that AFNetworking doesn't provide the resizing functions but returns the image in two different blocks: one before post-processing and one after post-processing.

2 or 3 MB is way too large for a thumbnail picture in a UITableViewCell. And you are re - creating a cell each time. You should use
[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
to improve performance

Related

Objective C - Multithreading issue

In my app, when user presses on the one of the tabs in UITabBar, it takes too much time to load view and show it to the user, so it may be confusing (it's because I load images from the web in the UITableView). So, I decided to use multithreading to show view before all the images finished loading.
I am using this code:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[SetsCustomCell alloc] initWithFrame:CGRectZero];
}
// getting url
NSURL* imgUrl = [[NSURL alloc]initWithString:[[mainArray objectAtIndex:
indexPath.row]valueForKey:#"imageURL"]];
//put this url and current cell in the dictionary
NSDictionary* params = [NSDictionary dictionaryWithObjectsAndKeys:
imgUrl,#"localUrl",cell,#"localCell", nil];
// multithreading time (calling loadImageWithParams method)
[self performSelectorInBackground:#selector(loadImageWithParams:)
withObject:params];
return cell;
}
-(void)loadImageWithParams:(NSDictionary*)params {
NSURL* url = [params objectForKey:#"localUrl"];
cell = [params objectForKey:#"localCell"];
UIImage* thumb = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
NSDictionary* backParams = [NSDictionary dictionaryWithObjectsAndKeys:
cell,#"localCell",thumb,#"thumb", nil];
[self performSelectorOnMainThread:#selector(setImage:)
withObject:backParams waitUntilDone:YES];
}
-(void)setImage:(NSDictionary*)params{
cell = [params objectForKey:#"localCell"];
UIImage* thumb = [params objectForKey:#"thumb"];
[cell.imageView setImage:thumb];
cell.imageView.hidden = NO;
[cell setNeedsLayout];
}
I have only two cells in the UITableView and the problem is that only second cell loads its image. The first cell is still empty. However, if I scroll the UITableView until the first cell is no longer visible, and it calls cellForRowAtIndexPath: again, first cell gets its image.
I have also tried to make multithreading with NSOperationQueue and GCD but have the same results.
It seems like I don't clearly understand how multithreading works but I will be veeeery grateful if someone will point me at my mistake.
Thanks!
The way I've done this in the past is to create an NSObject subclass to store the images. Then the datastore for the UITableView is an array of these objects instead of URL paths.
Then you can lazy load the image like this...
ObjectModel.h
#property (nonatomic, strong) UIImage *image;
ObjectModel.m
- (UIImage*)image
{
if (_image == nil) {
[self downloadImage];
return [UIImage imageNamed:#"placeholderImage"];
}
return _image;
}
- (void)downloadImage
{
//Put your async stuff here to download the image then reload the tableView when it's done.
}
By doing this you are storing the images on the datastore but also managing the download stuff in its own class and keeping your TVC clean.
It seems like you're overcomplicating this -- I don't really understand what you're doing with the params dictionary. Why not present the controller, then start your download in viewDidAppear. When the results come back, populate your array with them, and call reloadData. Do the cells have anything in them besides the images that you can load right away?
Typical practice for this is to implement a LazyUIImage class which downloads/loads the image in the background. While the image is downloading, display something like a UIActivityIndicator. When the image arrives in the LazyUIImage, send out a notification and stop the activity indicator (and remove it from the view perhaps). For every view controller that contains a LazyUIImage, handle that notification, and e.g., perform [myTable reloadData]. In LazyUIImage, write a method called getImage which returns nil while the image is loading, and after the image has loaded, return the image.
Edit: actually, if you implement the LazyUIImage as a LazyUIImageView then you don't need to reload anything in containing view controllers: in the LazyUIImageView place a top level background view, and then switch out the UIActivityIndicator child view for the actual UIImageView once the image finishes downloading.
You need to do Lazy loading stuff.. Search for lazy loading using NSOperationQueue.

How to display a complex UIViewController inside another UIView Controller?

Ok, so here's the situation. I currently have a view controller called MainViewController which has a UITableView with many different cells. When I click on a cell, I want that cell to expand (grow in height) and show some "additional information". The problem is, this additional information is very complex and can contain UILabels, other UITableViews, UIWebViews and UIImageViews. Furthermore, this "additional data" requires quite a bit of computation in order to determine what exactly to display (i.e. what the UILabels say, how large the UIImageViews are). Therefore, because of the complexity of this "additional information", I'm at a loss as to how to design my program.
The "additional information" requires a lot of code, thus I don't want to just throw that code into the MainViewController. Additionally, it would be nice if there was some way to use Interface Builder to design these "additional information" views graphically rather than programatically.
Currently I have each set of additional information as its own separate UIViewController (thus allowing me to have separate classes for the data and allowing me to use interface builder) and I just segue to a new screen when a cell is selected. However, I don't want to segue to a new screen; I want all of the data that this UIView controller is showing to be shown in MainViewController. What's the best way to do this?
In summary, I currently have one UIViewController segueing to another UIViewController; however, I want the second UIViewController's content to be show in the first. If possible I would like to use some sort of Interface Builder and to separate out the logic for this second UIViewController into another class.
Details:
~ I'm developing for iOS 5 only and I'm using ARC.
~ I've never developed for iOS 4 or below before and I have never used nib files before but I would be willing to learn if required. Simple sample code would be helpful.
~ Thanks!
Same opinion as SmartWork.
You should create your custom UITableViewCell class with its xib file, with a UITableViewCell as main xib view
And in your tableView datasource, you can import it as below :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"MyViewCell";
MyViewCell *cell = (MyViewCell *)[self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"MyViewCell" owner:nil options:nil];
for (id currentObject in topLevelObjects) {
if ([currentObject isKindOfClass:[UITableViewCell class]]) {
cell = (MyViewCell *)currentObject;
break;
}
}
}
[self configureCell:cell atIndexPath:indexPath]; // your own function to customize your cell.
return cell;
}
Then, in the cell xib, you can set the max height of the cell, and decide the effective height in the UITableViewDelegate class.
There are some good suggestions here, but note that loadNibNamed:owner: is a fairly expensive API to call repeatedly because it reads the nib from the filesystem each time. Instead, consider doing something like this.
Register your nib file in viewDidLoad. In Interface Builder, make sure to provide a reuse identifier for your custom cell.
- (void)viewDidLoad
{
[super viewDidLoad];
UINib *myNib = [UINib nibWithNibName:#"MyNib" bundle:nil];
[self.tableView registerNib:myNib forCellReuseIdentifier:#"MyCell"];
// Do any other stuff you need to do...
}
Then just dequeue your custom cell whenever you need it. UINib will cache the nib file in memory to avoid reloading it from the filesystem each time.
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"MyCell"];
// Configure cell as needed...
return cell;
}
in my opinion there is no need of using one UiviewController inside another.
u can use Uiview with nib file so u can design these "additional information" views graphically. its very easy to implement and maintain it.
Just to add to what SmartWork said, when you tap a particular cell, you can update the height of that row using the following lines of code:
- (CGFloat)tableView:(UITableView *)aTableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// set a dynamic value for the cell height depending on the state of the data in the cell
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// update the state of the data in the cells here..
// calling these below lines will change the height of the cells smoothly
[tableView beginUpdates];
[tableView endUpdates];
}
You will also need custom UITableViewCells. Look at them as simple views and add and remove any number of subviews that you need
If you are keen on using Nibs for your subviews inside the cells, you can create their nibs and connect them to your Custom TableView Cells as follows: (The subviews can be properties of your tableViewCell)
NSArray *nibArray = [[NSBundle mainBundle] loadNibNamed:#"MyTableViewCellNib" owner:self options:nil];
mySubview = [(SubView *)[nib objectAtIndex:0]retain];

UITableViewCell not updating properly

I've created a custom UITableViewCell but am having trouble updating the contents of the cell. When I have multiple cells in the table, the table is not drawing the correct images in the cell. The images in each cell should be unique, however I am seeing different cells with the same image. The table seems to be placing the cells at random.
I've checked my data source with NSLog and the names are correct. I can correct the issue when I don't use - (UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier, but instead create a new cell each time in - (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath.
Any suggestion on what I may be doing wrong? Please have a look at my code below.
- (UITableViewCell *)tableView:(UITableView *)_tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
ScoreCell *cell = (ScoreCell *)[_tableView dequeueReusableCellWithIdentifier:#"CellID"];
if (cell == nil)
{
cell = [[[ScoreCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"CellID"] autorelease];
}
BoxScore *boxScore = [_gameDayData objectAtIndex:indexPath.row];
[cell setScoreImage:[UIImage imageNamed:boxScore.name]];
return cell;
}
ScoreCell.h
#interface ScoreCell : UITableViewCell
{
UIImage *scoreImage;
}
#property(nonatomic, retain)UIImage *scoreImage;
#end
ScoreCell.m
#implementation ScoreCell
#synthesize scoreImage;
- (void)dealloc
{
[scoreImage release], scoreImage = nil;
}
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
[scoreImage drawAtPoint:CGPointMake(5,5)];
}
#end
In addition to the other comments, be sure to implement the -prepareForReuse method in your ScoreCell class. It gets called when the cell is to be reused, at which point you should clear the image. Be sure to call [super prepareForReuse]; in your implementation. This will prevent the cell from being reused with the wrong image.
There are two non-related problems with your image handling.
Scrolling off and on the screen will cause a cell to load the image twice (or a hundred times, depending on the user).
You want a
- (UIImage *)boxScoreImageForIndex:(NSInteger)index
method to (lazy) load, hold on to, and provide the image for the cell.
You also don't want to use imageNamed:, in your case it will cause twice as much memory usage than needed. Use imageWithContentsOfFile: instead.
You are not clearing the previous image. When a cell is dequeued it is not dealloced.
So sometimes the image that was drawn on the cell the times before it show through in front of the new image.
In drawRect you need to clear everything.
Either do:
CGContextClearRect( context , [self bounds] );
Or set clearsContextBeforeDrawing on the cell when it is created.

Table View with Images, slow load and scroll

I tried impletmenting about 30 tutorials today and just cant get anything to work.
My problem is that i load my information via a JSON file, add the data to a NSMutableArray, then use a table to display it all. It works fine when i dont have the images, but when i do its loads very slow and scrolls very sticky. I sorta understand after todays findings that its reloading the images every scroll which is why its slow.
Can someone please break it down and make it easier for me to solve this problem?
Alex
Take a look at Apple's LazyTableImages example. Basically it comes down to
a) reusing your table cells
b) only loading images that are currently visible
You kinda left your problem wide open b/c you weren't specific enough. Performance issues can be related to a bunch of things.
Here are a few performance things with tableview cells & images
• Load images on a background thread.
• Reuse cells - don't allocate any more than you need on screen
static NSString *CellIdentifier = #"Cell";
CellClass *cell = (CellClass*)[tv dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) cell = [[[CellClass alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
• Only draw images that are the same size of the cell (ie. if a cell is 44 px high, keep UIimages at 44px). If images are bigger, you might have to process the images after downloading them from the internet.
• Don't use uiimageview in your cell. instead create a custom cell (ie. subclass) and draw the image in your drawRect: function.
You should use asynchronous image retrieval provided by UIImageView categories found in AFNetworking or SDWebImage. These categories:
are incredibly easy to use (rather than using the UIImageView method setImage, instead use one of the categories' setImageWithURL methods);
provide asynchronous image retrieval;
cache the downloaded images with NSCache, to make sure you don't have to retrieve images that you just downloaded;
ensure that your UI cannot get backlogged downloading images for cells that have scrolled off screen; and
leverage operation queues to constrain the degree of concurrency (rather than using GCD global queues that can result in timeout failures).
I have a class that I call RemoteImageHandler. Here is the .h file:
#import <UIKit/UIKit.h>
#interface RemoteImageHandler : NSObject
- (void)imageForUrl:(NSURL*)url callback:(void(^)(UIImage *image))callback;
+ (RemoteImageHandler *)shared;
#end
And the .m file:
#import "RemoteImageHandler.h"
#interface RemoteImageHandler ()
#property (nonatomic, strong) NSMutableDictionary *imageDictionary;
#end
#implementation RemoteImageHandler
- (void)imageForUrl:(NSURL*)url callback:(void(^)(UIImage *image))callback {
if (!!self.imageDictionary[url]) {
callback(self.imageDictionary[url]);
} else {
dispatch_async(dispatch_get_global_queue(0,0), ^{
NSData * data = [[NSData alloc] initWithContentsOfURL:url];
if (data == nil)
callback(nil);
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *image = [UIImage imageWithData:data];
self.imageDictionary[url] = image;
callback(image);
});
});
}
}
+ (TQRemoteImageHandler *)shared {
static TQRemoteImageHandler *shared = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shared = [[self alloc] init];
});
return shared;
}
#end
In my table view, whenever I want an image from a remote location (let's say this is in cellForRowAtIndexPath, I use this:
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier forIndexPath:indexPath];
[[RemoteImageHandler shared] imageForUrl:someURLCorrespondingToTheImageYouWant callback:^(UIImage *image) {
cell.imageView.image = image;
}];
return cell;
}

How do I cache something for a tableview?

I have a tableview with large images that fill the cells and the row heights are set based on the image size. Unfortunately, the table jerks badly when scrolling to the next cell.
I've been told that my tableview will scroll more smoothly if I cache the row heights and the images before they are loaded into the table.
All my data are stored in a plist.
How do I go about caching something?
What does the code look like and where does it go?
Thanks!
Here's my code for loading the images:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *detailTableViewCellIdentifier = #"Cell";
DetailTableViewCell *cell = (DetailTableViewCell *)
[tableView dequeueReusableCellWithIdentifier:detailTableViewCellIdentifier];
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"DetailTableViewCell" owner:self options:nil];
for(id currentObject in nib)
{
cell = (DetailTableViewCell *)currentObject;
}
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSString *Path = [[NSBundle mainBundle] bundlePath];
NSString *MainImagePath = [Path stringByAppendingPathComponent:([[appDelegate.sectionsDelegateDict objectAtIndex:indexPath.section] objectForKey:#"MainImage"])];
cell.mainImage.image = [UIImage imageWithContentsOfFile:MainImagePath];
return cell;
}
I'm also using the following for calculating the row height:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
AppDelegate *appDelegate = (DrillDownAppAppDelegate *)[[UIApplication sharedApplication] delegate];
NSString *Path = [[NSBundle mainBundle] bundlePath];
NSString *MainImagePath = [Path stringByAppendingPathComponent:([[appDelegate.sectionsDelegateDict objectAtIndex:indexPath.section] objectForKey:#"MainImage"])];
UIImage *imageForHeight = [UIImage imageWithContentsOfFile:MainImagePath];
imageHeight = CGImageGetHeight(imageForHeight.CGImage);
return imageHeight;
}
EDIT: Here is the final code below.
#define PHOTO_TAG 1
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Photo";
UIImageView *photo;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
UIImage *theImage = [UIImage imageNamed:[[appDelegate.sectionsDelegateDict objectAtIndex:indexPath.section] objectForKey:#"MainImage"]];
imageHeight = CGImageGetHeight(theImage.CGImage);
imageWidth = CGImageGetWidth(theImage.CGImage);
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
photo = [[[UIImageView alloc] initWithFrame:CGRectMake(0, 0, imageWidth, imageHeight)] autorelease];
photo.tag = PHOTO_TAG;
[cell addSubview:photo];
} else {
photo = (UIImageView *) [cell viewWithTag:PHOTO_TAG];
[photo setFrame:CGRectMake(0, 0, imageWidth, imageHeight)];
}
photo.image = theImage;
return cell;
}
Caching is not a panacea for tableview performance. Caching is only valuable if there is something expensive to calculate, and you can avoid calculating it. If, on the other hand, you simply have too many views in your UITableViewCell, then caching will do nothing for you. If your row heights are all the same, then there's nothing to cache. If you use +[UIImage imageNamed:], then the system is already caching your images for you.
The most common first-order problem with UITableViewCells is putting too many subviews in them. How have you constructed your cell? Have you spent time studying the Table View Programming Guide, particularly A Closer Look at Table-View Cells? Understanding this document will save you much grief later.
EDIT: (Based on code above)
First, you're fetching a reusable cell, and then immediately throwing it away, reading a NIB and iterating over all the top level objects looking for a cell (one that looks almost exactly like the one you just threw away). Then you work out a string, which you use to open a file and read the contents. You do this every time UITableView wants a new cell, which is a lot. And you do it over and over again for the same rows.
Then, when UITableView wants to know the height, you read the image off of disk again. And you do that every time UITableView asks (and it may ask many times for the same row, though it does try to optimize this).
You should start by reading the UITableView Programming Guide I link above. That's hopefully going to help a lot. When you've done that, here are the things you should be thinking about:
You indicated that there is nothing but a single image view in this cell. Do you really need a NIB for that? If you do stick with a NIB (and there are reasons to use them in some case), then read the Programming Guide about how to implement a NIB-base cell. You should be using IBOutlet, not trying to iterate over the top-level objects.
+[UIImage imageNamed:] will automatically find files in your Resources directory without you having to work out the bundle's path. It will also cache those images for you automatically.
The point of -dequeueReusableCellWithIdentifier: is to fetch a cell that UITableView is no longer using and that you can reconfigure rather than you making a new one. You're calling it, but you immediately throw it away. You should check if it returned nil, and only load it out of the NIB if it did. Otherwise, you just need to change the image. Again, read the Programming Guide; it has many, many examples of this. Just make sure that you really try to understand what -dequeueReusableCellWithIdentifier: is doing, and don't treat it as just something you type at this point in the program.
If you do need to cache the heights, I did something like this (caching heights for a cell displaying an "article" object - article maybe one of several subclasses):
+ (CGFloat) heightForArticle: (Article*) article atWidth: (CGFloat) width {
static NSCache* heightCache = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
heightCache = [NSCache new];
});
NSAssert(heightCache, #"Height cache must exist");
NSString* key = #"unique"; //Create a unique key here
NSNumber* cachedValue = [heightCache objectForKey: key];
if( cachedValue )
return [cachedValue floatValue];
else {
CGFloat height = 40;//Perform presumably large height calculation here
[heightCache setObject: [NSNumber numberWithFloat: height] forKey: key];
return height;
}
}