5

I have been struggling with this for months in my project.

Here's the deal: I have a UITableView with just 1 section, but the heights of the UITableViewCells differ. Each UITableViewCell represents an object, and new objects are constantly becoming available, and inserted into the UITableView. The only way to effectively discover the height of a new object which will be displayed in the table is to draw it once (run the code that draws the UITableViewCell and look at what the resulting height is).

Ah, but there's the problem! The UITableView delegate method tableView:heightForRowAtIndexPath: is called BEFORE the cell is drawn for the first time! This makes sense: the table wants to position the cells before drawing them.

I've been experimenting LOTS and can find only 2 options, both of which have major downsides:

  1. If the new object to be shown in the table has not been seen before, simply "guess" the height and resize the cell later (after the cell is drawn) as needed. This causes problems withs scrolling though, since when the user stops scrolling it might be the case that one or more cells need to be resized to the correct height, which creates a bad user experience as the table shifts around and re-organizes itself.
  2. Pre-Calculate the object height by "testing" the object before inserting it into the UITableView. When the new object becomes available, create a "dummy" UITableViewCell and draw it off-screen, thus calculating the height of the object and saving it for later. The problem with this is that, of course, we can only do the height testing on the main thread (since we need to draw), which causes major lag in the application.

My solution thus far has been to use #2, but to try to alleviate the problem by making it so that the height-tests only happen every 1/4 of a second, which helps to "space out" the testing, so that the main thread is not locked too much. But, the solution is inelegant and still causes lag problems, especially on older devices. The lag is bad enough that I really need a better solution.

ROMANIA_engineer
  • 54,432
  • 29
  • 203
  • 199
Zane Claes
  • 14,732
  • 15
  • 74
  • 131
  • 2
    Perhaps it would hep if you explain what the new object is, in case the height can be calculated, without actually drawing it? – MadhavanRP Mar 22 '12 at 16:54
  • No. It cannot. The incoming object is a very complex object, a custom object, consisting of many many different moving parts. Trust me, this was my first thought as well and I most certainly would have done this if I could have. In fact, I spent weeks trying to create a good "guessing algorithm" (per #1, in my post). – Zane Claes Mar 22 '12 at 16:56

4 Answers4

2

For calculating the height, this and this answer suggest you could draw off-screen in a background thread using CGLayer. Link to iOS documentation to CGLayer.

And to cache the height depending on some data id that is being displayed in a cell you could use code like this:

-(CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath
{
    NSString* objectId = [self getObjectIdForPath:indexPath];
    if (![self->idToCellHeight objectForKey: objectId])
    {
        CGFloat height = [object calculateHeightForId:objectId];
        [self->idToCellHeight setObject:[NSNumber numberWithFloat:height] forKey: objectId];
    }

    return [[self->idToCellHeight objectForKey:objectId] floatValue];
}

Of course the first draw will still have to be done without knowing the exact height, but if you can't calculate the height based on the data displayed then there is no workaround.

Community
  • 1
  • 1
Greg
  • 8,230
  • 5
  • 38
  • 53
  • Of course, I'm already saving/caching the heights of the objects in this way. Drawing the post off-screen on a background thread would be a perfect solution, except that I can't seem to find any resources on how to get this to work with UIKit. To test the object's height, I'm creating an instance of a custom UITableViewCell subclass, which has a number of UIKit components (UILabel, UIImageView, etc.) Whenever I try to do this drawing operation on the background thread, I get a crash... – Zane Claes Mar 22 '12 at 17:43
  • UIKit in general is not thread safe, e.g. calling UIKit methods on non-main threads will confuse the rendering engine that is updating the UI from the main thread at the same time. However, some of the UIKit methods have become thread-safe on iOS 4.0. See this post: http://www.cocoabuilder.com/archive/cocoa/296299-drawing-thread-safety-in-ios.html – Greg Mar 23 '12 at 09:59
  • Also this link: http://stackoverflow.com/questions/6087068/thread-safe-uikit-methods and Apple documentation on this subject: http://developer.apple.com/library/ios/#releasenotes/General/WhatsNewIniPhoneOS/Articles/iPhoneOS4.html So I guess in your case it is a matter of figuring out which UIKit methods you can use to draw in the background thread. Granted, it may not be possible if your code is using many of them and they can't be easily replaced for thread-safe equivalents. – Greg Mar 23 '12 at 10:03
  • After lots and lots of work I managed to use proper thread-safe UIKit stuff to accomplish the pre-calculation. Major PITA, but ultimately worth it. I'm marking this as an accepted answer. – Zane Claes Apr 19 '12 at 19:57
1

Thought I would pitch in on this. Pre-calculation of row heights for a UITableView is entirely possible and works very well for optimisation.

As an alternative to off screen drawing, it's perfectly fine to use the sizeThatFits methods of classes such as UILabel to determine heights.

The caveat is that almost all UIView related methods must be performed on the main thread.

I'll explain my approach, which is to create a sizing view that I use for height calculation.

The sizing view can be created off the main thread:

UILabel *mySizingLabel = [[UILabel alloc] init];

However, when determining the size, that must be done on the main thread. Be careful not to use a dispatch_async or [[NSOperationQueue] mainQueue]. This for example will not work:

__block CGSize size;
dispatch_async(dispatch_get_main_queue(), ^{
    size = [mySizingLabel sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)];
}];

CGFloat myCellHeight = size.height;

That's because the line that assigns myCellHeight will execute before the sizeThatFits, thereby giving a nonsense value.

Instead, it's necessary to wait on the main thread until sizeThatFits returns. That can be done using performSelectorOnMainThread with the wait set to YES. Parameters can be passed in and out of that method using an NSMutableArray.

This approach has a big benefit in that you can take advantage of the inherent sizing methods offered by UIView classes, without the need to draw offscreen using a custom graphics context.

One other thing, don't be tempted to use the UIKit addition sizeWithFont. Apart from the issues highlighted by Mike Weller in "You're Doing It Wrong #2: Sizing labels with -[NSString sizeWithFont:...]", the same caveat applies that this method must also be executed on main:

NSString UIKit Additions Reference

The methods described in this class extension must be used from your app’s main thread.

Max MacLeod
  • 26,115
  • 13
  • 104
  • 132
0

I'll try to explain briefly.

Like Max MacLeod indicates, when you have your dataset ready calculate row height asynchronously using

-(void)calculateAsyncRowHeights {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        UITableView *tableView=[self tableView];

        for(ManagedObject *o in _tableData) {
            CGFloat height=[self calculateRowHeight:tableView withObject:o]; //this is the method that calculates row heights. This is a seperate method because when necessary I call it in - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
            [o setRowHeight:height]; //I set rowHeight property of my model object
        }

        dispatch_async(dispatch_get_main_queue(), ^{

        });
    });
}

In - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath check if the model object has the row height already calculated. If yes, return it, if no, calculate it by [self calculateRowHeight:tableView withObject:o]; method and return its return value.

It works like a charm and solved all of my performance problems.

Cagatay Gurturk
  • 7,186
  • 3
  • 34
  • 44
-1

It sounds to me that you should create an NSMutableArray that contains NSNumber items that correspond to the calculated heights of the cells that exist in the table. When you get a new cell that needs to be added to the table, calculate the height of the item and then insert the height into the mutable array at the proper position, or in other words, at the same position that it will be occupying in the table, and then reload the table data.

That way, you do not have to recalculate any heights, you would just reference the object at the index of your index path row.

BP.
  • 10,033
  • 4
  • 34
  • 53
  • The problem is *not* about *RE*-calculating heights... it is about the FIRST calculation. I am already saving the calculated heights, after the first calculation, as you described here. You said "When you get a new cell that needs to be added to the table, calculate the height of the item." *THAT* is the part that is causing the trouble, though! – Zane Claes Mar 22 '12 at 17:06
  • Ah, I guess I missed where you said "saving it for later" and thought that you were doing too much work in heightForRowAtIndexPath, silly me. – BP. Mar 22 '12 at 17:59