cellforRowAtIndexPath efficiency? - ios

Whenever I scroll my tableview it is very laggy. I think it has to do with how I am loading up my cells. I use UINib (5.0+) whenever I can while still providing backwards compatibility. Then I load my custom cell's labels and images with items from a NSDictionary from a NSArray which is loaded from NSUserDefaults in the ViewDidLoad.
Is there any way to improve the efficiency of this cellForRowAtIndexPath?
- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomCell *cell = (CustomCell *)[aTableView dequeueReusableCellWithIdentifier:#"Cell"];
if (cell == nil) {
if ([self labelCellNib]) {
[[self labelCellNib] instantiateWithOwner:self options:nil];
} else {
[[NSBundle mainBundle] loadNibNamed:#"CustomCell" owner:self options:nil];
}
cell = [self CustomTableCell];
[self setCustomTableCell:nil];
}
NSDictionary *dictionary = [myArray objectAtIndex:indexPath.row];
NSData *data = [dictionary objectForKey:#"OCRImage"];
cell.previewPicture.image = [self roundCorneredImage:[UIImage imageWithData:data] radius:60];
cell.titleLabel.text = [dictionary objectForKey:#"Title"];
cell.titleLabel.delegate = self;
cell.dateLabel.text = [dictionary objectForKey:#"Date"];
if (indexPath.row%2) {
cell.backgroundImage.image = firstImage;
}
else {
cell.backgroundImage.image = secondImage;
}
return cell;
}
Edit:
- (UIImage*)roundCorneredImage: (UIImage*)orig radius:(CGFloat) r {
UIGraphicsBeginImageContextWithOptions(orig.size, NO, 0);
[[UIBezierPath bezierPathWithRoundedRect:(CGRect){CGPointZero, orig.size}
cornerRadius:r] addClip];
[orig drawInRect:(CGRect){CGPointZero, orig.size}];
UIImage* result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
Edit2: These are the lines that are causing the lag:
NSData *data = [dictionary objectForKey:#"OCRImage"];
cell.previewPicture.image = [self roundCorneredImage:[UIImage imageWithData:data] radius:60];

As #Till said in a comment, you should launch your app in Instruments (Product -> Profile in Xcode), and select the CPU -> Time Profiler instrument.
Then, scroll around over the place for a few seconds, then hit the Record toolbar icon in instruments to close your app. You will be able to see the scrolling section because CPU usage will probably be pinned at 100% (unless it's slow because of network activity problem).
Click on the timeline after the start of the high CPU activity area, and click the "start inspection range" toolbar button, then click before the end of the high CPU activity area and click the "stop inspection range" toolbar button.
You can now drill down into the call tree view at the bottom of the window to figure out exactly where all your CPU usage is. In my experience it's usually easier to find the problem if you turn off "invert call tree" option on the left.
Performance bugs can be very hard to find, and sometimes a line of code that is obviously slow actually isn't causing any problems at all. The only way to fix performance issues without wasting time is to use Instruments.

Make sure that you've set the reuse identifier for your cell to the same thing that you've specified in your code, i.e. #"Cell". If they don't match, then you won't be reusing cells properly, and probably spending a lot more time creating cells than necessary.
If you are properly recycling cells, then you should take a look at the code after the if (cell == nil) {...} block. You'll be skipping that entire block once the table has created enough cells to fill the screen (and maybe one or two more), so most of the time attributable to this method while scrolling will be due to the following code. It'd be interesting to know what myArray is, and if it's actually an array, what the objectForKey: method does. Nothing else there looks like it should take a long time, but the best way to find out where the cycles are going is to profile your code in Instruments.

Some of my notes after looking at your code:
Is roundCorneredImage:radius: caching the result? If not, executing CG calls for every cell would surely present a bottleneck. Updated: Use instruments to be sure, but it might be faster (memory allowing) to store the processed UIImage in a collection so that you can pull it out again the next time that method is called with the same parameters.
All of your UIImages could be declared elsewhere and then presented in this method. Your current code instantiates a new UIImage for each cell which can also bottleneck your scrolling. Updated: Since Image1.png and Image2.png are basically static, you could declare them in your interface or as a static ivar and then just assign them to the background image rather than instantiating UIImage each time.
It may be faster to subclass UITableViewCell and instantiate that instead of reaching into UINib. Also, you'd then be able to separate your layout/data logic from the delegate method. Here's a gist of what I did in my UITableViewCell subclass. Basically, I store the entity with the cell and the cell knows about it's labels and such. This keeps the cell layout logic out of my data source code.
It looks like you're using an NSDictionary as your data source. If you have a lot of objects in that dictionary, it may be considerable faster to use CoreData and an NSFetchedResultsController. Here's a good post on the matter. Updated: Ok, that shouldn't be an issue.
-
Edit
So if you removed all of this:
NSDictionary *dictionary = [myArray objectForKey:#"OCRImage"];
cell.previewPicture.image = [self roundCorneredImage:[UIImage imageWithData:data] radius:60];
if (indexPath.row%2) {
cell.backgroundImage.image = firstImage;
}
else {
cell.backgroundImage.image = secondImage;
}
and it still lags, let's look at your constructors...what do these lines do?
cell = [self CustomTableCell];
[self setCustomTableCell:nil];
Also, you're not using any transparent images or anything in your table cell are you? Those have been known to cause drawing lag...
-
Edit #2
If you strip down to this, what happens?
CustomCell *cell = (CustomCell *)[aTableView dequeueReusableCellWithIdentifier:#"Cell"];
if (cell == nil) {
if ([self labelCellNib]) {
[[self labelCellNib] instantiateWithOwner:self options:nil];
} else {
[[NSBundle mainBundle] loadNibNamed:#"CustomCell" owner:self options:nil];
}
cell = [self CustomTableCell];
[self setCustomTableCell:nil];
}
cell.titleLabel.text = [dictionary objectForKey:#"Title"];

Related

Custom UITableViewCell isn't released or reused after navigation, memory leak

I have a class that defines a custom contact cell called ContactItemCell and a xib file that lays out that cell. When I create the table view it opens up and six of these ContactItemCell classes are created. When I navigate back they aren't deallocated, and when I open the tableview again another 6 are created. Here's the tableview code:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
Contact* contact = [self contactAtIndexPath:indexPath];
static NSString *cellID = #"ContactItemWithTagsForBothCell";
ContactItemCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
if (cell == nil)
{
NSArray *arr = [[NSBundle mainBundle] loadNibNamed:#"ContactItemWithTagsForBothCell" owner:nil options:nil];
if (arr.count <= 0)
{
NSLog(#"couldnt find cell with ID: %#", cellID);
return nil;
}
cell = [arr firstObject];
}
[cell configureCellForContact:contact];
return cell;
}
The fileowner in the xib file is just set to be blank which I'm assuming means NSObject. I've tried looking for a strong reference cycle to see if the class is kept alive by pointers but I haven't seen anything after days of investigation. I'm really at my limit and I'm not sure what else I can do, I've been using instruments too and that's how I've figured out that they're being created 6 at a time but I can't find out what's pointing to them. Why is this happening? Am I doing something wrong with the table view? If I'm not and you think it's a strong reference cycle then how can I find every object that points to this ContactItemCell? Thanks in advance!
If anyone was curious I used the memory debugger and found that there was a strong reference cycle with a pod I was using.

How to create custom cell without using reuse Identifier

I am making a program in which I am fetching a image from URL and displaying on the custom cell and rest of the table data on other cells.
Here is the code:
image=[[UIImage alloc]init];
if(indexPath.row==0)
{
LabelTableViewCell *cell=[detailtable dequeueReusableCellWithIdentifier:#"imagecell" forIndexPath:indexPath];
NSOperationQueue *myqueue=[[NSOperationQueue alloc] init];
NSBlockOperation *downloadOperation = [NSBlockOperation blockOperationWithBlock:^{
[cell.actindi startAnimating];
image=[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[frontimage objectAtIndex:0]]]];
}];
downloadOperation.completionBlock=^{
cell.imageprimary.image=image;
[cell.actindi stopAnimating];
};
[myqueue addOperation:downloadOperation];
return cell;
}
else
{
LabelTableViewCell *cell = [detailtable dequeueReusableCellWithIdentifier:#"tabledetail" forIndexPath:indexPath];
cell.labelspec.text=[firstArray objectAtIndex:indexPath.row];
cell.txtfield.text=[secondArray1 objectAtIndex:indexPath.row];
return cell;
}
return 0;
Problem I am facing:
Whenever I scroll up it reloads the top cell.
It dont reload the image on first cell till I click on that cell.
if(!cell) and if (cell==nil) not working.
Reloading the table puts in an infinite loop till I scroll down.
So I want to permanently allocate the memory to the first cell as it is only one in number.
I have tried putting nil in dequeueReusableCellWithIdentifier and its not working.
I have gone about 20-30 articles to do that but nothing has worked for me I don't know why.
So please give me any specific solution at the present condition.
I am new in the iOS developing don't know too much about it . This is my first program that I am trying to make.
You can try it different way:
To download and show image Just use library - https://github.com/rs/SDWebImage
Library will download image and cache for you.

Don't reload cell when scroll back

I have an issue that i didn't find anywhere on the web. I have to display some custom cell loaded from a Nib. I download the informations from my DB on a separated thread and allocate it on a new MutableArray.
I have also images that are allocated in a separate array and called when necessary, but not downloaded from the web.
My table view is "lagging" when scrolled down, that is (i guess) because it has to place the things on the correct cell, but when i scroll back it lags again and it reloads again all informations.
I see that Facebook app loads cells when scrolling down (but not so slowly) and when scrolled back it doesn't reload anything and cells are already loaded (no matter how many). How can i do something like this? My table is very slow and i have (at the moment) only 3 cells.. But when the application is finished these would be 100 or 200.
Can anyone help me?
This is my code: (this on viewDidLoad)
NSString *strURL = [NSString stringWithFormat:#"..(myurl)..];
// to execute php code
NSData *dataURL = [NSData dataWithContentsOfURL:[NSURL URLWithString:[strURL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]];
// to receive the returend value
NSString *strResult = [[NSString alloc] initWithData:dataURL encoding:NSUTF8StringEncoding];
if(![strResult isEqualToString:#"0"]) {
posts = [strResult componentsSeparatedByString:#"^"];
}
immProfilo = [NSMutableArray array];
for (int i = 0; i < [posts count]; i++) {
NSArray *datiPost = [[posts objectAtIndex:i] componentsSeparatedByString:#"/"];
FBProfilePictureView *fotoProfiloFB = [[FBProfilePictureView alloc] initWithFrame:CGRectMake(22, 22, 55, 55)];
fotoProfiloFB.profileID = [datiPost objectAtIndex:1];
[immProfilo addObject:fotoProfiloFB];
}
[self.postTab reloadData];
And that is my tableview code:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)postTab {
return [posts count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *CellIdentifier = [NSString stringWithFormat:#"%ld_%ld",(long)indexPath.section,(long)indexPath.row];
PostTabCell *cell = (PostTabCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"PostTabCell" owner:self options:nil];
cell = [topLevelObjects objectAtIndex:0];
}
NSString *annuncio = [posts objectAtIndex:indexPath.section];
datiAnnuncio = [annuncio componentsSeparatedByString:#"/"];
[cell addSubview:[immProfilo objectAtIndex:indexPath.section]];
cell.nome.text = [datiAnnuncio objectAtIndex:0];
cell.location.text = [self getAddressFromLatLon:[[datiAnnuncio objectAtIndex:2] floatValue] withLongitude:[[datiAnnuncio objectAtIndex:3] floatValue]];
cell.testoPost.text = [datiAnnuncio objectAtIndex:4];
return cell;
}
The reason why your table view is so laggy is because each time the table view asks the delegate for a cell (your cellForRowAtIndexPath method), you perform a synchronous network request with getAddressFromLatLon, blocking the main thread.
An immediate fix would be to -at least- store these texts in some kind of array, so that next time the table view asks for the same cell you don't have to perform a network request again.
This would solve the problem of the tableview being laggy when you scroll back up, but not when you scroll down the first time. One general rule you can always consider true, is that you shouldn't ever block the main thread with network requests.
You've got two options now: load all of these texts at the very beginning on a secondary thread while showing a spinner (easy but presents several problems, such as it wouldn't scale up very well with the number of cells). Or you would have to design an asynchronous loader that will show a placeholder string, such as loading address..., until the address is actually loaded.
Also Totumus has a point in his answer, but that is not the main cause of your lag (although it will be a big problem once the number of your cells increase).
The lag you describe is caused by the imageview you keep adding to your reusable cell. Everytime the cell reloads an imageview is added to your cell again.
Prevent functions like addSubview: in your cellForRowAtIndexPath:.
Instead you should create a custom cell that already has this UIImageView you need for your profilepictures.
The reason you load information when you scroll is that you probably load information (getAddressFromLatLon: ?) each time a cell is being created/reused. This is fine but should be done in a seperate thread (and thus the response should be handled asynchronically).
The reason you see facebook not loading anymore when you scroll back up again is because they cache their data when it is loaded into the application. Probably with CoreData.

IOS image is not set in an image view

I am using the same code in two of my view controllers (they are implementing the same class what changes is the url they download) and in one occassion the image is displayed correclty while in the other I do see an empty cell.
Here is my code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleTableIdentifier=#"MyCell";
//this is the identifier of the custom cell
MyCell *cell = (MyCell *)[tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
tableView.backgroundColor=[UIColor clearColor];
tableView.opaque=NO;
tableView.backgroundView=nil;
if (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"MyCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
}
NSLog(#"Image url is:%#",[images_url objectAtIndex:indexPath.row]);
NSURL *url_image=[NSURL URLWithString:[images_url objectAtIndex:indexPath.row]];
cell.myimage.image=[UIImage imageWithData:[NSData dataWithContentsOfURL:url_image]];
return cell;
}
As i told you I have 2 view controllers implementing the same class. In the view did load the url is set depending on the value of a flag. If I open controller A, I see no image, if I open view B i can see the image. Both of the urls are correct as I can check it with the NSLog I have inserted.
What might be the problem?
Unfortunately calling "NSData dataWithContentsOfURL" is a blocking call. Execution of your program will stop until iOS is able to fetch all the data from the server or fails trying. This may often be "fast" if you're on LTE or WiFi; but can potentially take a LONG time.
Meanwhile, you're on the "main thread" in your app - so your app will appear to freeze-up, and the system's watchdog timer may kill your app. If anyone besides you will use this ap, you absolutely need to populate your tableview cell's image with local data that's retrieved immediately or use asynchronous methods.
Just google for "lazy load UIImage". This SO question has some good tips on the subject:
lazy-load-images-in-uitableview
Additionally, you should move these lines to some setup code. You don't need to perform them every time to update a cell:
tableView.backgroundColor=[UIColor clearColor];
tableView.opaque=NO;
tableView.backgroundView=nil;
Best of luck!

updating a UI table view cell with upload status - iOS

Hell everyone :)
My experience with the UITablewView Controller in iOS is unfortunately quite limited. What I need in my application is a UI table view which contains one custom cell for each active upload currently being uploaded to a webserver (videos, audio, etc).
Each of these uploads run asynchrounously in the background, and should all be able to update things such as UILabels in their respective cells saying something about the update progress in percentage, etc.
Now I have found a solution which works. The problem is I do not know if it is actually secure or not. Based on my own conclusion I don't really think that it is. What I do is simply to retrieve a reference of the UIViews from a cell which is getting created, and then store those references in the upload objects, so they can change label text and so on themselves.
My Own Solution
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CustomCellIdentifier = #"CustomCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: CustomCellIdentifier];
if (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"UploadCellView" owner:self options:nil];
if ([nib count] > 0)
{
cell = customCell;
}
else
{
NSLog(#"Failed to load CustomCell nib file!");
}
}
NSUInteger row = [indexPath row];
UploadActivity *tempActivity = [[[ApplicationActivities getSharedActivities] getActiveUploads] objectAtIndex:row];
UILabel *cellTitleLabel = (UILabel*)[cell viewWithTag:titleTag];
cellTitleLabel.text = tempActivity.title;
UIProgressView *progressbar = (UIProgressView*)[cell viewWithTag:progressBarTag];
[progressbar setProgress:(tempActivity.percentageDone / 100) animated:YES];
UILabel *cellStatusLabel = (UILabel*)[cell viewWithTag:percentageTag];
[cellStatusLabel setText:[NSString stringWithFormat:#"Uploader - %.f%% (%.01fMB ud af %.01fMB)", tempActivity.percentageDone, tempActivity.totalMBUploaded, tempActivity.totalMBToUpload]];
tempActivity.referencingProgressBar = progressbar;
tempActivity.referencingStatusTextLabel = cellStatusLabel;
return cell;
}
As you can see, this is where I think I'm doing something which isn't quite good enough:
tempActivity.referencingProgressBar = progressbar;
tempActivity.referencingStatusTextLabel = cellStatusLabel;
The upload activities get a reference to the controls stored in this cell, and can then update them themselves. The problem is that I do not know whether this is safe or not. What if the cell they are refering to gets re-used or deleted from memory, and so on?
Is there another way in which you can simply update the underlying model (my upload activites) and then force the UI table view to redraw the changed cells? Could you eventually subclass the UITableViewCell and let them continously check up against an upload and then make them upload themselves?
EDIT
This is how the upload activity objects calls their referencing UI controls:
- (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten
totalBytesWritten:(NSInteger)totalBytesWritten
totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite
{
if (referencingProgressBar != nil)
{
[referencingProgressBar setProgress:(percentageDone / 100) animated:YES];
}
if (referencingStatusTextLabel != nil)
{
[referencingStatusTextLabel setText:[NSString stringWithFormat:#"Uploader - %.f%% (%.01fMB ud af %.01fMB)", percentageDone, totalMBUploaded, totalMBToUpload]];
}
}
My only concern is that, since these objects run asynchrounously, what if at some given point the UI table view decides to remove or re-use the cells which these upload objects are pointing to? It doesn't seem very secure at all.
There are two possibilities, assuming you have a background process that is uploading:
The tableview is a delegate and implements some uploadProgress
function
The tableview listens for uploadProgress NSNotifications
The second is easier to implement, just put the listeners start/stop in viewdidappear/viewdiddissappear. Then in your upload you can track progress and emit a notification with attached userinfo that gives an integer value to progress. The table has a function that handles this notification being received and redraws the cells. Here is how to add data to the userinfo part of an NSNotification.
If you wanted to be fancier you could have an upload id and map this to a cell index, and only redraw that particular cell. Here's a question and answers that explain how to do this.
Disgusting Pseudocode Since I don't have access to my IOS dev env right now
upload function:
uploadedStuff{
upload_id = ... // unique i, maps to row in table somehow
byteswritten = ...
bytestotal = ....
userinfo = new dict
userinfo["rowid] = upload_id
userinfo["progress"] = (int)byteswritten/bytestotal
sendNotification("uploadprogress",userinfo)
}
tableview.m:
viewdidappear{
listenForNotification name:"uploadprogress" handledBy:HandleUploadProgress
}
viewdiddisappear{
stoplisteningForNotification name:"uploadprogess"
}
HandleUploadProgess:NSNotification notification {
userinfo = [notification userinfo]
rowId = [userinfo getkey:"rowId"]
progress = [userinfo getkey:"rowId"]
// update row per the link above
}

Resources