I need to optimize UITableViewCell image loading - ios

I have a custom UITableViewCell. Each cell has an unique image.
I have an image loader class. I passed a dictionary to ImageLoader's method. A dictionary composed by image url and some other information. This code works well when we scroll down UITableView slowly. But if we scroll down rapidly it will have some lags. I need to optimize my algorithm. Any ideas.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
[ImageLoader addImageToQueue:dictionary];
}

I believe that this tutorial would be useful in answering your question. It covers the reasons why you aren't able to scroll smoothly and how to fix it.

Disable the image loading and test the table behavior - the problem might be a slow cell initialization, if the custom cell drawRect is overridden, pay much attention to this method as the documentation says it's navy usage slows the animations dramatically, find the way to optimize the drawing.
If the table scrolls smoothly with the image-loading disabled, consider to limit the number of the concurrent load operations (with the pending operations to be loaded later) and use caching whenever available.

Use UITableView dequeueReusableCellWithIdentifier:
This basically means that you don't have to recreate the table cell from scratch every time. If you give a cell a reuse identifier, it will keep ahold of that cell in memory and let you dequeue it when you ask for a cell matching that identifier.
Since every cell has a unique image, you'd probably be best off setting each cell's unique identifier as something from the image - perhaps the image's URL or filename. This will eat up more memory than disposing of and recreating the cells, but it reduces scroll lag, and if your images are small then the memory footprint isn't that large.
You'd probably end up with something like:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *imageURL = urlOfImageForThisCell;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:imageURL];
if(!cell)
{
cell = [self makeNewCell];
//if your reuseIdentifier is unique to each image, you only ever need to set it's image here
cell.image = imageForThisCell;
cell.reuseIdentifier = imageURL;
}
//additional cell setup here
return cell;
}
You'll also want to look in subclassing UITableViewCell and overriding prepareForReuse.

I would check out Ray Wenderlich's tutorial on Multithreading and GCD here.
It's hard without seeing your cellForRowAtIndexPath: or ImageLoader code but I have a suspicion that your ImageLoader is slowing things down to the point where it has to wait for it to complete before it provides the cell. Even if it's just reading the image from the disk, it should be done on the background thread, asynchronously so that the main thread can focus on scrolling smoothly.
Good luck!

I know this is old, but I recommend this post:
http://www.nsprogrammer.com/2013/10/easy-uitableview-buffering.html

Related

how to fix an issue with custom subclass of uitableviewcell reuse?

Before I get crucified for this, let me state... Yes, I have read every "relevant" answer on this topic and have not found a workable solution. Most "correct" answers are pre-ARC and discuss "releasing" a cell, which just isn't done anymore. Secondly, my problem is not "global", meaning some views have no problems, while others do. So here is my question...
I have sub-classed uitableviewcell and setup some uilabels & custom uiviews. From there I wired everything up in ib (Xcode 5.x iOS 7.x). Once I put in the appropriate code and create the tableview & dynamic cells from a nsarray "not mutable" everything works exactly as expected with no issues.
This is the fun part. I am making changes to allow the data source of the tableview, which is an nsarray to be mutable to allow adding and removing of items / cells. This is where things get hairy. When I start to add more objects to the array and when the reuse cell is being put on screen visual data from old cells is being reused on new cells. I say "visual" because once the cell is selected the view updates to display the correct information. The part that is interesting is that as I stated I have some uilabels which never have any problems being redisplayed, my custom views however are now the piece of the puzzle that is displaying info from past cells, and when scrolling back up, the original cells no longer display the correct information. Once the cell is tapped, then the cell updates and displays the correct information.
the most confusing bit of this is that before my array was mutable and had a static amount of objects this worked fine. Even if a cell went off screen and came back, it was still the correct information being displayed. Now I know that shouldn't have anything to do with it, but it is strange that it worked using the same tableview & cell code that I am using now.
I have tried adding in
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
if (!cell) {
cell = [[MyCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"Cell"]; // note: obviously as stated, tacking on "autorelease" here as mention in other suggestions is not going to work.
}
Which doesn't fix the issue.
I tried overriding the "prepareForReuse" method on my custom cell subclass and that does not resolve the issue either. I have made the views, "strong" & "weak", and all that and still every 3rd or so cell gets repeated with garbage data until it is refreshed. Again, the uilabels which are setup the same way as the views have no problems and data is never reused. I would say there is a problem with my custom views, but setting up the table from a static source of identical information there is no problem.
I would like to post some code, but it's all pretty generic code for tableviews & delegates. any suggestions would be greatly appreciated.
As i said the code is all pretty generic, but apparently it needs posing anyway so here it is..
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyThing *thing = self.stuffArray[indexPath.row];
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.thisLabel.text = thing.someText;
cell.thatLabel.text = thing.otherText;
cell.view1.someProptery = thing.object1.property
cell.view2.someProptery = thing.object2.property
cell.view3.someProptery = thing.object3.property
//"someProperty" on "view..." is an NSInt that is used to determine custom drawing in the view.
return cell;
}
I think the key to the solution lies in your comment about the custom views in the cells. If cellForRowAtIndexPath is altering the states of those views, they need to know that they must be redrawn, so you'll need to augment the synthesized setter in your custom view.m that has someProperty.
If the someProperty determines how this view get's drawn, then it's incumbent upon the setter to indicate that the view is out of date....
- (void)setSomeProperty:(NSInteger)someInt {
_someProperty = someInt;
[self setNeedsDisplay];
}

How do I stop a server call when a user scrolls too quickly?

I am developing an iPhone application that retrieves photos from a server and presents them on the screen similar to the way Pulse News Reader presents its articles. I have a vertically scrolling UITableView and each UITableViewCell contains its own custom UITableView instance that allows the user to thumb through photos horizontally.
The way that I currently populate the horizontally-scrolling table is by making a server call in the cellForRowAtIndexPath method of the vertically-scrolling table. Whenever a new cell comes into the view, I send a request for that particular cell's photos, as in the code below:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"EventCell";
EventViewCell *cell = (EventViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[EventViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
Event *event = [self.fetchedResultsController objectAtIndexPath:indexPath];
[self performPhotoFetchForEventID:event.id inCell:cell withRetries:3];
return cell;
}
This solution works really well when the user scrolls through the vertical table slowly, however if the user scrolls quickly or "flicks" through the list, it begins to break down. Essentially, if the user scrolls through X events really quickly, the app will fire off X server requests and return all of the results, even though the app's view only needs the results of at most three of those requests. This occasionally causes pictures to show up in the wrong rows, or show a delay.
How can I prevent / cancel my server requests from firing if the user scrolls too quickly. Or is there a better way to implement this to get the desired effect?
This occasionally causes pictures to show up in the wrong rows
Before tackling other parts of your problem, you should try to understand why this is happening.
From my experience, (of course depends on the size of the image), it's better to just get the Image from the server and not cancel it. Although you should keep something in mind:
Cache it once you have it, and make sure to check your cache before asking for a new. You can use a NSDictionary and the key be the image's URL.
Async calls. You shouldn't block/stress your UI Thread with the Request/Cache part. Use NSOperations and NSOperationQueue to create small workers to take care of the request and caching. Once you get it, just update your UIImageView.
Leave a Placeholder while the Image is being fetch. This shows the user that something is happening.
Update your DataSource and not your Cells. The reason why, is that if you are seeing rows [0,1,2,3,4,5] and you scroll and now you are seeing rows [40,41,42,43,44,45], when the responses comes with the images for the first rows, the cells have been long gone (might be the problem your experiencing with the Images on the wrong places).
Finally, and because you mention Pulse News, why not learn a bit with them? Use this link to access their Engineering Page and learn a bit about downloading images. :)
If you are using NSUrlConnection to retrieve images in a lazy loading mode, you can send this command to the connection:
[connection cancel];
Here the Apple docs
You should not, my all means, create photo request on every cellForRowAtIndexPath:. You should use async image caching lib, there are few of them: I use JMImageCache.

Xcode 4.5 UITableView

I have a question regarding UITableView on Xcode. I have text in the cell but when I test it, it doesn't show. I just started programming so any help would be appreciated. If anyone can email me so I can provide the project file that would be most helpful.
You will need to be aware of how table views work. I would suggest carefully reading some of the excellent online documentation on this topic, including sample code if available.
You will need to remember that the system is responsible for deciding which parts of the table view are supposed to be visible, then asking your code to provide the details of what should be seen.
If you have actually provided the routine to provide the contents of the cell, try putting in a breakpoint or some logging to ensure that your code is being called. If so, start by returning something simple and verifying that this is displayed.
Also, prepare code with an explanation of what it is doing to add to the question. You never know, just doing that may clarify to you what is going wrong. If not, post it up and give someone else a chance to help you.
I cannot really directly solve your problem because you surprisingly haven't given code so as far as I know you could be missing a semicolon! (JK)
This is a detailed answer I gave to someone a while ago, very relevant to your situation
But for a table view use this code:
- (UITableViewCell *)tableView:(UITableView *)tableView2 cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:#"UITableViewCell"];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:#"UITableViewCell"]
autorelease];
cell.textLabel.text = nil;
}
if (cell) {
//customization
cell.textLabel.text = #"Text Label";
}
return cell;
}
You say you are starting coding.... let me explain. First of try picking up the book:
The Big Nerd Ranch Guide
I suggest you read a heck load of books to get you juiced up on language and API and programming skills
Dequeuing is basically nilling and cacheing any cell that is not visible, aka you scroll past it. Therefore, cell == nil will be called in probably four situations (that I can think of):
When we first setup the table view (cells will be nil)
When we reload data
Whenever we may arrive at this class
When the cells becomes invisible from the table view
So, the identifier for dequeuing is like an ID. Then in the statement to see if cell is nil, we initialize cell, you can see the overridden init method: initWithStyle. This is just what type of cell there is, there are different types with different variables you can customize. I showed you the default. Then we use the reuseIdentifier which was the dequeuing identifier we said earlier. THEY MUST MATCH! I nil textLabel just for better structure, in this case each cell has the same text so it won't matter really. It makes it so the cell that dequeues comes back with the right customization you implemented. Then once cell is actually valid, we can customize.
Also, you are using the same text for each cell. If you do want to have different text for each cell, familiar yourself with NSArray. Then you could provide the array count in numberOfRowsForSection and then do something like this:
cell.textLabel.text = [array objectAtIndex: [indexPath row]];
Where indexPath is the NSIndexPath argument provided in the cellForRowAtIndexPath method. The row variable is the row number, so everything fits!
Wow, that was a lot to take in right! Now go stop being an objective-c noob and start reading some books!
For more info read:
Table View Apple Documentation

UITableView issue (iOS)

I wonder why cellForRowAtIndexPath function is called when scrolling the UITableView. Does it mean on every scrolling cell configuration code runs again? I have a slowness problem when scrolling the table.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"CountryCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
// Configure the cell...
NSString *continent = [self tableView:tableView titleForHeaderInSection:indexPath.section];
NSString *country = [[self.countries valueForKey:continent] objectAtIndex:indexPath.row];
cell.textLabel.text = country;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
return cell;
}
As others have said, yes, when each new cell is about to scroll onto the screen, cellForRowAtIndexPath is called for each cell. While this may strike you as a performance hit, the alternative (for iOS to create all of the cells up-front) would be worse: If you're finding it slow to create one cell, imagine having to create hundreds, many of which the user may never see. Anyway, the just-in-time nature of cellForRowAtIndexPath is probably more efficient, both in terms of the up-front performance hit, as well as in terms of precious memory consumption on a small mobile device.
In terms of slow performance, there's nothing here that could be the culprit. A lot of us have cellForRowAtIndexPath methods that are significantly more complicated and speed is not an issue. Maybe the there are more efficient alternatives to a valueForKey lookup that I might advise if you were doing millions of lookups, but for one cell the difference would not be observable to the human eye. I think you have to look elsewhere. Is titleForHeaderInSection doing anything strange (i.e. something other that just looking up a value in an array)? Maybe you can share that with us. Maybe something in the UI. Maybe you should run this through the Profiler's "Time Profiler" and maybe something will stick out. But a cellForRowAtIndexPath this simple should result in a perfectly smooth user experience.
When you scroll the table, cells that dissappear are discarded and cells that appear must be created and configured using cellForRowAtIndexPath.
Or better said - cells are not discarded, they are moved to a queue of cells that can be reused and no new cells are created - the reusable cells are taken from the queue and reconfigured.
The method cellForRowAtIndexPath gets called for each and every row in your UITableView, when a row disappears from the visible view, its memory is cleared and a new cell that is coming into visibility, for that new memory is allocated (or a older one might be re used) and the new cell is configured using cellForRowAtIndexPath method.
It comes to know from this method only that what needs to be configured for this particular cell.
It is slow may be because of for each cell you are performing some heavy operation.
It's correct that cellForRowAtIndexPath is called when scrolling the table, that's exactly what it's for. Every time a new cell shows up during scrolling, a UITableViewCell instance should be created or re-used, and then configured.
Slowness is usually caused by performing an expensive operation in the configuration phase. In your case, the potentially slow operation would be the country lookup.

How to make a UITableView row grow dynamically with the user modified content

I am asking this question already knowing the answer is "no you can't" but in the hopes someone has a brilliant idea here we go.
I have a subclass of UITableViewCell that has a few different subviews one of witch is a UITextField that I have as user editable. So naturally the textField grows with the text that is entered.
Now the question is how could I get the tableview row to grow with the textField.
I have a variety of different size cells so I know how to use - (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath but the problem is that I need to modify the after the cell is already in the tableview.
Also note that the previously noted delegate method gets called before the data source delegate method - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
I have thought of just pulling up another view for the user to edit then putting that data into my data model then I would have to force the tableview to reload. (Can I reload just a certain cell/row)
Interestingly enough I think apple is doing what I want in their iTunes U app. When you tap a assignment it expands. I think they are using tableviews their right?
I know I have a lot of questions and talk here but I believe it is all related and just to show what I have researched. I am just looking for the possibility that one of you has a stroke of genius.
Hopefully this can help others also because this seems to be a hot topic but no one ask the question well or gets good answers.
Actually, this has been asked and answered many times -- it's not impossible. Search for "UITableView custom row height" or "UITableView multi line UITextField" or similar and you'll find several well-answered questions.
You're on the right track -- you need to return a height in tableView:heightForRowAtIndexPath:, even though that method is called before you create/configure the cell. This is okay... you just need a way to compute that height without having the cell. Other answers reference -[NSString sizeWithFont:constrainedToSize:] and related methods... this should get you on the right track.
Changing the height while the text field is editing is less obvious, but this answer has the key... if you call
[tableView beginUpdates];
[tableView endUpdates];
the table view will not only ask its delegate for heightForRowAtIndexPath: again and resize the cell to match, it'll do it with a smooth animation.

Resources