How to build my cells inside a thread from a delegate? - ios

I have this tableView and I want to build the cells in a custom way.
The way I do it is by adding subviews to the cell.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if(cell == nil)
{
cell=[[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier] autorelease];
}
else
{
UIImageView* img=[UIImage imageNamed:#"abc.png"];
[cell addSubview:img];
//processing the final cell takes time!
}
return cell;
}
But, since some jittery frame rate is affecting the responsiveness of the table, I'd like to pass all this creation of the cell into a thread. so I want to put an image of some sort, for the meantime, and once finished, the thread would update cell to be the final cell.
Is this a normal approach?
If so, how do I update the cell from the thread?
Do I need to define all variables as __block? before going into the thread?

since some jittery frame rate is affecting the responsiveness of the table, I'd like to pass all this creation of the cell into a thread.
The vast majority of UIKit, which includes UITableViewCell, isn't thread safe. From Apple's documentation:
For the most part, UIKit classes should be used only from an application’s main thread. This is particularly true for classes derived from UIResponder or that involve manipulating your application’s user interface in any way.
...only a few drawing and graphics context methods in UIKit are thread safe. It's a really bad idea to create your cells on a secondary thread. You don't want to be doing this.
There's a lot of material out there are ways to efficiently create and draw cells into table views - as someone has pointed out in the comments, you seem to be adding a UIImageView not at cell creation, but on re-use. This means your cells are going to have many, many image views being constantly added to them. This is obviously not a good idea.
A better idea is to create a UITableViewCell subclass that already contains the views you're going to need, or to add them at creation and access them via view tags.

Related

Using TableView with many custom Cells and autolayout

I want to refactor some Code for better Performance, but my Problem is i´m not sure how to do it. At the Moment i have one UIViewController with a UIScrollView on it.
I also have 20 different Views which (each has its on .h and .m File) can be laid fully dynamically on my UIScrollView. Every Time i start the UIViewController i send a request to my Server and then i get the Response and then i know how many Views i have to put on the UIScrollView.
So you can imagine when theres a lot of different views on my UIScrollView it takes a few seconds because alle Views are getting fully loaded, before the User can finally interact with them.
So my idea is to replace the UIScrollView with a UITableView and change all the CustomViews (UIViews) to Custom UITableViewCells. So only the visible Cells would be loaded at the first start!
Know i have several Problems.
At the Moment, most of the Code from my CustomViews are build with Frames, but i want to change it completely to Autolayout, i don´t think it makes sense to build them all with the IB (xib files ...). So i have to do the whole Autolayout Stuff in Code?
Some of the Custom Views are really big, so they getting really high, and some can be really small. My concern is that the scrolling Perfomance will be really bad... because i cannot really use estimatedRowHeight(A Example: sometimes one Cell can get a height of 1000.0f and the next cell only 40.0f).And in Combination with Autolayout and the Time i have to wait until the Response frome my Server arrives, i think it could be really annoying for the User.
There can be up to 20 different!! custom rows, makes it really sense to use a UITableView in this case? As i mentioned before, they are all very different in their Size and Content!
Here is a little Part of my new Code:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.node.blocks count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
GTBlockView *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
GFBlock *block = [self.node.blocks objectAtIndex:indexPath.row];
//createInterfaceforBlock -- Here the Cell gets called and the Content and the Size gets defined
cell = [[GTAppController sharedInstance] createInterfaceForBlock:block];
// Make sure the constraints have been added to this cell, since it may have just been created from scratch
[cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
//GTBlockView is the SuperView of all my Custom Cells
GTBlockView *cell = [self.offscreenCells objectForKey:CellIdentifier];
if (!cell)
{
cell = [[GTBlockView alloc] init];
[self.offscreenCells setObject:cell forKey:CellIdentifier];
}
GFBlock *block = [self.node.blocks objectAtIndex:indexPath.row];
//createInterfaceforBlock -- Here the Cell gets called and the Content and the Size gets defined
cell = [[GTAppController sharedInstance] createInterfaceForBlock:block];
// Make sure the constraints have been added to this cell, since it may have just been created from scratch
[cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];
cell.bounds = CGRectMake(0.0f, 0.0f, CGRectGetWidth(tableView.bounds), CGRectGetHeight(cell.bounds));
[cell setNeedsLayout];
[cell layoutIfNeeded];
// Get the actual height required for the cell
CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
height += 1;
return height;
}
Maybe some of you have some better Ideas or some good Sources?
I have done exactly as you are thinking in an app of my own - I first went down the UIScrollview route, then I changed to a UITableview with many kinds of custom cells. It does make total sense and is very worth it - I gained a massive performance increase from the uitableview. One of the major problems with the UIScrollview is that it will work out the autolayout for all the content in your contentView, which as you say, can take several seconds, but the UITableview will handle this much more quickly and efficiently. So - go for it.
I would strongly suggest you use a unique XIB file for each custom cell. Do the autolayout in each one individually, and that way you are much more likely to avoid problems down the line. Programmatic constraints are far harder to maintain, and autolayout is often challenging.
To get it all working, I did the following : first I had a subclass of UITableviewCell that was the parent class for all the other cell objects. In my case this was called SNFormTableCell (UITableviewCell). Then all other cell objects were based on this class. In my cellForRowAtIndexPath method, I do this :
ReportItem *objectForCell = [self.reportSet reportItemForSection:indexPath.section andRow:indexPath.row]; //my personal data class - just contains the cell data I need
NSString *identifier = [self getNibNameAndReusableIdentifierNameForObjectType:objectForCell.cellType.integerValue];
SNFormTableCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell)
{
[self registerNib:[UINib nibWithNibName:identifier bundle:nil] forCellReuseIdentifier:identifier];
cell = [tableView dequeueReusableCellWithIdentifier:identifier];
cell.contentView.clipsToBounds = YES;
}
[cell setSnFormTableCellDelegate:self]; //i have a delegate that calls back to the tableview when cells are interacted with
cell.reportItem = objectForCell; //put the data object onto the cell to do with as the cell requires
[cell refreshUI]; //update the UI using the data - this is over-ridden by the various subclasses of the cell
The method in there called getNibNameAndReusableIdentifierNameForObjectType .. looks like this (just gets the identifier we need for nib and re-use):
- (NSString*) getNibNameAndReusableIdentifierNameForObjectType:(SNFormTableObjectType)objectType {
if (objectType == SNFormTableObjectTypeBoolean) return #"SNBooleanCell";
if (objectType == SNFormTableObjectTypeDatePicker) return #"SNDatePickerCell";
if (objectType == SNFormTableObjectTypeDropDown) return #"SNDropDownCell";
if (objectType == SNFormTableObjectTypeDropDownPlusSingleLineText) return #"SNDropDownPlusSingleLineTextCell";
... etc
}
Then finally, the parent cell class has a method called -(void) refreshUI. So I put the data object onto that cell - this contains all the data I might need for the cell. The the subclasses over-ride this refreshUI method in their own specific way, to use the data as they need to.
Just to re-iterate, I gained enormously from going down this route. A scrollview with a lot of content, taking 5 or more seconds to load the nibs and calculate the autolayout (on the main thread too, making the app unresponsive), would appear instantly on the UITableview version. So go for it. If you need any more details on how to go about it, let me know.

UITableViewCell being reused while still on-screen?

I have what probably seems like a really weird problem (it does to me!)
I am using a UITableView to display cells which each contain a UIWebView. I realise that this is a bad idea on the face of it, but I can't really do this any other way.
I am caching the heights of each cell when the UIWebView finishes loading, and then calling:
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:#[cellIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
All of the germane code is in a Gist here.
I also have the UIWebViews cached in a dictionary on the data source, so it can be reused when the cell is reloaded.
This seems to sort of work, but I am encountering a lot of issues whereby the cells' contents will randomly disappear. I have added some logging into determine what's going on, and in what order, and it seems like some of the cells are being reused while they're still on-screen.
I see this in my logs while scrolling down:
2014-02-11 13:45:49.091 EApp[45936:70b] Generating cell for 1: Panning
2014-02-11 13:45:49.245 EApp[45936:70b] Generating cell for 2: Calibration
2014-02-11 13:45:50.063 EApp[45936:70b] Generating cell for 3: Aperture Priority
2014-02-11 13:45:50.063 EApp[45936:70b] Reusing cell: Stopping down
"Stopping down" in this case is a cell that is still on-screen. The "generating cell" items are logged inside the data source's cellForRowAtIndexPath and the "reusing" messages inside the cells' prepareForReuse.
Does anyone know what could be happening here? I know this seems complex.
The following line in your prepareForReuse is probably the culprit:
if ([self.contentWebView isDescendantOfView:self.contentWebView]) {
[self.contentWebView removeFromSuperview];
}
As the contentWebView is never a descendant of itself, it will not be removed from the cell, and the contentView will contain two webviews after the cellForRowAtIndexPath:
You probably meant to say:
if ([self.contentWebView isDescendantOfView:self.contentView]) {
[self.contentWebView removeFromSuperview];
}
Or simply:
[self.contentWebView removeFromSuperview];
One of the features/limitations of UITableView is that you don't know if, and can't depend on, a cell is being created or reused. You should always be able to handle both.
GENERALLY, when you call -reloadRowsAtIndexPaths:withRowAnimation:, you will get the cell from that indexPath to reuse. If that indexPath was on screen, it will be a cell that was on screen.
I don't know if it's the problem, but in the code you provided, you don't even initialize your cell...
I'm even surprise it works.
static NSString *CellIdentifier = #"FeedItemCell";
EFeedItemCell *cell = [self.tableViewController.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
you should add to it :
if (!cell) {
cell = [EfeedItemCell alloc] initWithReus....];
}
From your code it seems that you are caching the webViews and then are adding them to cells programmatically. This can create random problems similar to what I had faced in the passed.
You must use EFeedItemCellWebView in your storyboard. Just add a UIWebView and change the class name to your custom class. And then when the data is loaded just simply change its contents in - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

iOS dequeueReusableCellWithIdentifier is always returning null

I'm really frustrated at this point. Dequeueing a reusable cell with identifier is always returning null.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil) {
NSLog(#"INIT");
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
return cell;
}
What am i doing wrong here? Thanks.
You're doing everything right, everything is working as it should. iOS will create enough new cells to fill the screen (plus one). It will start reusing these cells only when your UITableView contains more rows than can fit on one screen and then the user scrolls.
You'll find that if you have a datasource will say, 100 items in it and then scroll, you'll only have your log message show probably 11 times (depends on how many cells fit on your screen) instead of 100 as iOS will start recycling cells as you scroll.
With large lists, it would use too much memory to create new views for every possible row in a UITableView. The alternative would be to allocate new views for rows as you scroll. However, this would create a performance bottleneck that would cause laggy scrolling in any UITableView.
Apple mention the performance bottleneck in their documentation on UITableViews.
Reuse cells. - Object allocation has a performance cost, especially if the allocation has to happen repeatedly over a short period—say, when the user scrolls a table view. If you reuse cells instead of allocating new ones, you greatly enhance table view performance.
Did you set your cell's reuse identifier? Init your cell with -initWithStyle:reuseIdentifier:, or set the identifier in IB.

scrolling on tableView is so slow

i have a tableview which has image and a text behind, i create the cells like below code :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
NSDictionary * companyProductRow = [DB getCompanyProductRow:[self.companyProductIDs objectAtIndex:indexPath.row]];
int companyProductID = [[companyProductRow objectForKey:#"ID"] intValue];
cell.tag = companyProductID;
cell.textLabel.text = [companyProductRow objectForKey:#"ImagePath"];
NSString* fullPath = [FileManager fullPath:companyProductID fairDirectory:[[self.currentFair objectForKey:#"ID"]intValue]];
[[cell imageView] setImage:[UIImage imageWithContentsOfFile:fullPath]];
return cell;
}
i read some tips about tableview performance on apple developer site but all of they said is :
Reuse cells. Object allocation has a performance cost, especially if the allocation has to happen repeatedly over a short period—say, when the user scrolls a table view. If you reuse cells instead of allocating new ones, you greatly enhance table-view performance.
Avoid relayout of content. When reusing cells with custom subviews, refrain from laying out those subviews each time the table view requests a cell. Lay out the subviews once, when the cell is created.
Use opaque subviews. When customizing table view cells, make the subviews of the cell opaque, not transparent.
the sample in apple site is the same, but i want to know is there any way to have better performance when scrolling on uitableview? (when we should read image from disk).
so thanks
Yes it is.
The first thing you could enhance is the image loading.
Try to avoid [UIImage imageWithContentsOfFile:fullPath] because it always loads the entirely image into the memory. This in fact is slow. Use [UIImage imageNamed:"YOUR_IMGAES_NAME"] instead because it caches the images after the first use or preload/store them directly in your fileManager.
The next step would be to set all views in the cell to nil (like the imageView) and draw all the contents by hand. The reason for this is that UIViews are pretty slow. If you have a lot of labels, images, etc. to display it happens to be much faster to draw everything by hand.
Your tableView:cellForRowAtIndexPath: looks like it's doing 2 disk reads for each cell, one from your [DB getCompanyProductRow:...] which I assume is a database fetch of some sort, and the second from the [UIImage imageWithContentsOfFile:...].
Find a way to load these information to memory, before tableView:cellForRowAtIndexPath: gets called (preferably before [tableview reloadData]).
Tips for large datasets:
If the data you need to fetch is too big for the memory to be loaded into an array, try to implement some paging mechanism so you just need to display a subset of that data. I would even recommend you use Core Data's NSFetchedResultsController instead, but that's if your database structure is compatible for object models.

Why is scrolling performance poor for custom table view cells having UISegmentedControl objects?

I have a UITableView with custom cells that were defined in the xib file, and am experiencing poor scrolling performance (choppy) on my device when the cells have a UISegmentedControl on them. NSLog statements reveal that the cells are being allocated and reused as they ought. My code for cellForRowAtIndexPath method is below. Connections are made in the xib as per Apple's documentation. (Scrolls smoothly in simulator btw)
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil)
{
[[NSBundle mainBundle] loadNibNamed:#"TableViewCell"
owner:self
options:nil];
cell = self.tvCell;
self.tvCell = nil;
}
cell.layer.shouldRasterize = YES; // build error is here
UILabel *lbl = (UILabel *)[cell viewWithTag:1];
[lbl setText:[NSString stringWithFormat:#"Q%i", indexPath.row+1]];
return cell;
}
Any drawing that a table cell has to do while it's being scrolled is going to cause performance issues; when you have a lot of subviews, there tends to be a lot of drawing going on, and that will—as you've observed—make your scrolling pretty choppy. There are a couple of ways to try to reduce that.
The first step is to make sure that your cells themselves, and as many of their subviews as possible, have their opaque properties set to YES. Opaque views don't have to get blended with the content underneath them, and that saves a lot of time.
You may also want to set your cells' layers to rasterize themselves, like this:
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
This will collapse your view hierarchy into one flat bitmap, which is the kind of thing Core Animation just loves to draw. Note that any animating views—activity indicators, for instance—will force that bitmap to be updated every time they change, i.e. a lot. In that case, you won't want the cell to rasterize everything; you might just use a subview with all of your relatively static views (e.g. labels) beneath another subview with any such dynamic content, and only have the first of those rasterized.
Make sure your identifier is 'MyIdentifier' in the xib. You'll get a good performance hit if it's not. I'm guessing that 'allocated and reused as they ought' means a few allocated on startup and no more allocated after. If that's true then you're probably all set.
Another way to improve performance is to construct your table view with code. It's a good deal faster than using xib's. When I construct table views I usually build them in IB, then copy over the frame values into code and construct in code.
Set aside some time to watch the WWDC 2010 performance video's. A lot of great information, I learn something new each time I watch them.

Resources