Iphone - 当每个单元格高度是动态时,何时计算tableview的heightForRowAtIndexPath?

时间:2011-01-27 23:29:57

标签: ios uitableview reloaddata heightforrowatindexpath

我已经多次看过这个问题,但令人惊讶的是,我没有看到一致的答案,所以我会自己尝试一下:

如果你的tableview包含你自己的包含UITextViews和UILabels的自定义UITableViewCells,其高度必须在运行时确定,你应该如何确定heightForRowAtIndexPath中每一行的高度?

最明显的第一个想法是通过计算并然后对cellForRowAtIndexPath内单元格内每个视图的高度求和来计算每个单元格的高度,并存储该最终总高度以供以后检索。

但是这不起作用,因为在AFT的heightForRowAtIndexPath中调用了cellForRowAtIndexPath。

我唯一能想到的是在viewDidLoad中进行所有计算,然后创建所有UITableViewCells,计算单元格高度并将其存储在UITableViewCell子类内的自定义字段中,并将每个单元格放入NSMutableDictionary中。 indexPath作为键,然后使用cellForRowAtIndexPath和heightForRowAtIndexPath中的indexPath从字典中检索单元格,返回自定义高度值或单元格对象本身。

这种方法似乎是错误的,因为它没有使用dequeueReusableCellWithIdentifier,而是我将所有单元格一次加载到我的控制器中的字典中,并且委托方法只会从中检索正确的单元格。字典。

虽然我没有看到任何其他方法。这是一个坏主意 - 如果是这样,这样做的正确方法是什么?

10 个答案:

答案 0 :(得分:60)

Apple实现UITableView的方式对每个人来说并不直观,而且很容易误解heightForRowAtIndexPath:的角色。一般意图是,这是一种更快,更轻松的内存方法,可以非常频繁地为表中的每一行调用。这与cellForRowAtIndexPath:形成对比,sizeWithFont:通常较慢且占用内存较多,但仅针对实际需要在任何给定时间显示的行进行调用。

为什么Apple会像这样实现它? 部分原因是,它几乎总是更便宜(或者如果编码正确可以更便宜)来计算行的高度,而不是构建和填充整个单元格。鉴于在许多表中,每个单元的高度都是相同的,它通常要便宜得多。 另一部分原因是因为iOS需要知道整个表的大小:这允许它创建滚动条并将其设置在滚动视图等上。

因此,除非每个单元格的高度相同,否则在创建UITableView时,无论何时向其发送reloadData消息,都会为每个单元格的数据源发送一条heightForRowAtIndexPath消息。因此,如果您的表有30个单元格,则该消息将被发送30次。假设屏幕上只显示这30个单元格中的6个。在这种情况下,当创建它并发送reloadData消息时,UITableView将为每个可见行发送一个cellForRowAtIndexPath消息,即该消息被发送六次。

有些人有时会对如何在不创建视图的情况下计算单元格高度感到困惑。但通常这很容易做到。

例如,如果您的行高不同,因为它们包含不同数量的文本,您可以使用相关字符串上的heightForRowAtIndexPath:方法之一进行计算。这比构建视图然后测量结果更快。请注意,如果更改单元格的高度,则需要重新加载整个表格(使用reloadData - 这将询问代表的每个高度,但只询问可见的单元格)或选择性地重新加载大小所在的行已更改(上次我检查时,也会在行上调用{{1}},但也会执行一些滚动工作以获得良好的衡量标准。)

请参阅this question,也许还有this one

答案 1 :(得分:10)

所以,我认为你可以做到这一点,而不必同时创建你的单元格(正如你所说,这对于大量单元格而言是浪费的,也可能是不切实际的。)

UIKit为NSString添加了几种方法,你可能已经错过了它们,因为它们不是主要的NSString文档的一部分。你感兴趣的是:

- (CGSize)sizeWithFont...

以下是Apple docs的链接。

理论上,这些NSString的添加存在于这个确切的问题:要弄清楚文本块占用的大小,而需要加载视图本身。您可能已经可以访问每个单元格的文本作为表视图数据源的一部分。

我说'理论上'因为如果您在UITextView中进行格式化,您的里程可能因此解决方案而异。但是我希望它能让你至少在某个方面。在Cocoa is My Girlfriend上有一个例子。

答案 2 :(得分:5)

我过去使用的一种方法是创建一个类变量来保存您将在表中使用的单元实例(我将其称为原型单元格)。然后在自定义单元格类中,我有一个方法来填充数据并确定单元格需要的高度。请注意,它可以是真正填充数据的方法的简单变体 - 而不是实际调整单元格中UILabel的大小,例如,它可以使用NSString高度方法来确定UILabel在最终单元格中的高度和然后使用总单元格高度(加上底部边框)和UILabel放置来确定实际高度。你可以使用原型单元来了解元素的放置位置,这样你就可以知道当标签高达44个单位时它意味着什么。

heightForRow:我然后调用该方法返回高度。

cellForRow:中,我使用实际填充标签并调整大小的方法(您自己从不调整UITableView单元的大小)。

如果你想获得想象力,你也可以根据传入的数据缓存每个单元格的高度(例如,如果确定高度,那么它可能只在一个NSString上)。如果你有很多通常相同的数据,那么拥有一个永久缓存而不仅仅是内存是有意义的。

你也可以尝试根据字符或单词数来估算行数,但根据我的经验,从来没有用过 - 当它出错时,它通常会弄乱一个单元格及其下面的所有单元格。

答案 3 :(得分:5)

这是我根据UTextView中的文本量计算单元格高度的方法:

#define PADDING  21.0f

- (CGFloat)tableView:(UITableView *)t heightForRowAtIndexPath:(NSIndexPath *)indexPath {

    if(indexPath.section == 0 && indexPath.row == 0)
    {   
        NSString *practiceText = [practiceItem objectForKey:@"Practice"];
        CGSize practiceSize = [practiceText sizeWithFont:[UIFont systemFontOfSize:14.0f] 
                   constrainedToSize:CGSizeMake(tblPractice.frame.size.width - PADDING * 3, 1000.0f)];
        return practiceSize.height + PADDING * 3;
    }

    return 72;
}

当然,您需要调整 PADDING 和其他变量以满足您的需求,但这会设置其中包含UITextView的单元格的高度,根据提供的文字数量。因此,如果只有3行文本,则单元格相当短,就好像有14行文本一样,单元格的高度相当大。

答案 4 :(得分:3)

我见过的最好的实现是Three20 TTTableView类的方式。

基本上他们有一个派生自UITableViewController的类,它将heightForRowAtIndexPath:方法委托给TTTableCell类上的类方法。

然后,该类返回正确的高度,总是通过执行与绘制方法相同的布局计算。通过将其移动到类中,它可以避免编写依赖于单元实例的代码。

实际上没有其他选择 - 出于性能原因,框架在要求它们的高度之前不会创建单元格,并且如果可能存在大量行,您实际上并不想这样做。

答案 5 :(得分:3)

将每个单元格的计算移动到tableView:heightForRowAtIndexPath:的问题是每次调用reloadData时都会重新计算所有单元格。方式太慢,至少对于我的应用程序来说可能有100行。这是一个使用默认行高的替代解决方案,并在计算它们时缓存行高。当高度发生变化或首次计算时,会安排表重新加载以通知表格视图新的高度。这确实意味着当行的高度发生变化时,行会显示两次,但相比之下这些行是次要的:

@interface MyTableViewController : UITableViewController {
    NSMutableDictionary *heightForRowCache;
    BOOL reloadRequested;
    NSInteger maxElementBottom;
    NSInteger minElementTop;
}

的tableView:heightForRowAtIndexPath:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // If we've calculated the height for this cell before, get it from the height cache.  If
    // not, return a default height.  The actual size will be calculated by cellForRowAtIndexPath
    // when it is called.  Do not set too low a default or UITableViewController will request
    // too many cells (with cellForRowAtIndexPath).  Too high a value will cause reloadData to
    // be called more times than needed (as more rows become visible).  The best value is an
    // average of real cell sizes.
    NSNumber *height = [heightForRowCache objectForKey:[NSNumber numberWithInt:indexPath.row]];
    if (height != nil) {
        return height.floatValue;
    }

    return 200.0;
}

的tableView:的cellForRowAtIndexPath:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Get a reusable cell
    UITableViewCell *currentCell = [tableView dequeueReusableCellWithIdentifier:_filter.templateName];
    if (currentCell == nil) {
        currentCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:_filter.templateName];
    }

    // Configure the cell
    // +++ unlisted method sets maxElementBottom & minElementTop +++
    [self configureCellElementLayout:currentCell withIndexPath:indexPath];

    // Calculate the new cell height
    NSNumber *newHeight = [NSNumber numberWithInt:maxElementBottom - minElementTop];

    // When the height of a cell changes (or is calculated for the first time) add a
    // reloadData request to the event queue.  This will cause heightForRowAtIndexPath
    // to be called again and inform the table of the new heights (after this refresh
    // cycle is complete since it's already been called for the current one).  (Calling
    // reloadData directly can work, but causes a reload for each new height)
    NSNumber *key = [NSNumber numberWithInt:indexPath.row];
    NSNumber *oldHeight = [heightForRowCache objectForKey:key];
    if (oldHeight == nil || newHeight.intValue != oldHeight.intValue) {
        if (!reloadRequested) {
            [self.tableView performSelector:@selector(reloadData) withObject:nil afterDelay:0];
            reloadRequested = TRUE;
        }
    }

    // Save the new height in the cache
    [heightForRowCache setObject:newHeight forKey:key];

    NSLog(@"cellForRow: %@ height=%@ >> %@", indexPath, oldHeight, newHeight);

    return currentCell;
}

答案 6 :(得分:2)

非常好的问题:寻找更多有关此问题的见解。

澄清问题:

  1. 行的高度在(cellForRowAtIndexPath)
  2. 之前调用
  3. 大多数人在CELL(cellForRowAtIndexPath)中计算身高类型信息。

  4. 有些解决方案非常简单/有效:

    • 解决方案1:强制heightForRowAtIndexPath计算单元格的规格。 Massimo Cafaro 9月9日

    • 解决方案2:对单元格执行第一次“标准尺寸”,在单元格高度时缓存结果,然后使用新的高度重新加载表格 - 对称

    • 解决方案3:另一个有趣的答案似乎是涉及到三个但是根据答案,似乎没有在storyboard / xib中绘制的单元格会使这个“问题”更容易解决。

答案 7 :(得分:0)

我接受了我最初提出的想法,这似乎工作正常,我在viewDidLoad中提前加载所有自定义单元格,将它们存储在NSMutableDictionary中,其索引为键。我发布相关代码,并且会喜欢任何人对此方法的任何批评或意见。具体来说,我不确定在viewDidLoad中从nib创建UITableViewCells的方式是否存在任何内存泄漏问题 - 因为我没有发布它们。

@interface RecentController : UIViewController <UITableViewDelegate, UITableViewDataSource> {

NSArray *listData;
NSMutableDictionary *cellBank;

}

@property (nonatomic, retain) NSArray *listData;
@property (nonatomic, retain) NSMutableDictionary *cellBank;
@end



@implementation RecentController

@synthesize listData;
@synthesize cellBank;

---

- (void)viewDidLoad {

---

self.cellBank = [[NSMutableDictionary alloc] init];

---

//create question objects…

--- 

NSArray *array = [[NSArray alloc] initWithObjects:question1,question2,question3, nil];

self.listData = array;

//Pre load all table row cells
int count = 0;
for (id question in self.listData) {

    NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"QuestionHeaderCell" 
                                                 owner:self 
                                               options:nil];
    QuestionHeaderCell *cell;

    for (id oneObject in nib) {
        if([oneObject isKindOfClass:[QuestionHeaderCell class]])
            cell = (QuestionHeaderCell *) oneObject;

            NSNumber *key = [NSNumber numberWithInt:count];
            [cellBank setObject:[QuestionHeaderCell makeCell:cell 
                                                  fromObject:question] 
                         forKey:key];
            count++;

    }
}

[array release];
[super viewDidLoad];
}



#pragma mark -
#pragma mark Table View Data Source Methods

-(NSInteger) tableView: (UITableView *) tableView
numberOfRowsInSection: (NSInteger) section{

return [self.listData count];

}

-(UITableViewCell *) tableView: (UITableView *) tableView
     cellForRowAtIndexPath: (NSIndexPath *) indexPath{

NSNumber *key = [NSNumber numberWithInt:indexPath.row];
return [cellBank objectForKey:key];


}

-(CGFloat) tableView: (UITableView *) tableView
heightForRowAtIndexPath: (NSIndexPath *) indexPath{

NSNumber *key = [NSNumber numberWithInt:indexPath.row];
return [[cellBank objectForKey:key] totalCellHeight];

}

@end



@interface QuestionHeaderCell : UITableViewCell {

UITextView *title;
UILabel *createdBy;
UILabel *category;
UILabel *questionText;
UILabel *givenBy;
UILabel *date;
int totalCellHeight;

}

@property (nonatomic, retain) IBOutlet UITextView *title;
@property (nonatomic, retain) IBOutlet UILabel *category;
@property (nonatomic, retain) IBOutlet UILabel *questionText;
@property (nonatomic, retain) IBOutlet UILabel *createdBy;
@property (nonatomic, retain) IBOutlet UILabel *givenBy;
@property (nonatomic, retain) IBOutlet UILabel *date;
@property int totalCellHeight;

+(UITableViewCell *) makeCell:(QuestionHeaderCell *) cell 
               fromObject:(Question *) question;

@end



@implementation QuestionHeaderCell
@synthesize title;
@synthesize createdBy;
@synthesize givenBy;
@synthesize questionText;
@synthesize date;
@synthesize category;
@synthesize totalCellHeight;







- (void)dealloc {
[title release];
[createdBy release];
[givenBy release];
[category release];
[date release];
[questionText release];
[super dealloc];
}

+(UITableViewCell *) makeCell:(QuestionHeaderCell *) cell 
                 fromObject:(Question *) question{


NSUInteger currentYpos = 0;

cell.title.text = question.title;

CGRect frame = cell.title.frame;
frame.size.height = cell.title.contentSize.height;
cell.title.frame = frame;
currentYpos += cell.title.frame.size.height + 2;


NSMutableString *tempString = [[NSMutableString alloc] initWithString:question.categoryName];
[tempString appendString:@"/"];
[tempString appendString:question.subCategoryName];

cell.category.text = tempString;
frame = cell.category.frame;
frame.origin.y = currentYpos;
cell.category.frame = frame;
currentYpos += cell.category.frame.size.height;

[tempString setString:@"Asked by "];
[tempString appendString:question.username];
cell.createdBy.text = tempString;

frame = cell.createdBy.frame;
frame.origin.y = currentYpos;
cell.createdBy.frame = frame;
currentYpos += cell.createdBy.frame.size.height;


cell.questionText.text = question.text;
frame = cell.questionText.frame;
frame.origin.y = currentYpos;
cell.questionText.frame = frame;
currentYpos += cell.questionText.frame.size.height;


[tempString setString:@"Advice by "];
[tempString appendString:question.lastNexusUsername];
cell.givenBy.text = tempString;
frame = cell.givenBy.frame;
frame.origin.y = currentYpos;
cell.givenBy.frame = frame;
currentYpos += cell.givenBy.frame.size.height;


cell.date.text = [[[MortalDataStore sharedInstance] dateFormat] stringFromDate: question.lastOnDeck];
frame = cell.date.frame;
frame.origin.y = currentYpos-6;
cell.date.frame = frame;
currentYpos += cell.date.frame.size.height;

//Set the total height of cell to be used in heightForRowAtIndexPath
cell.totalCellHeight = currentYpos;

[tempString release];
return cell;

}

@end

答案 8 :(得分:0)

以下是我在非常简单的情况下所做的事情,一个包含标签中的注释的单元格。音符本身被限制为我施加的最大长度,因此我使用多行UILabel并为每个单元动态计算正确的8,如以下示例所示。你几乎可以处理UITextView。

- (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];
    }

    // Configure the cell...
    Note *note = (Note *) [fetchedResultsController objectAtIndexPath:indexPath];
    cell.textLabel.text = note.text;
    cell.textLabel.numberOfLines = 0; // no limits

    DateTimeHelper *dateTimeHelper = [DateTimeHelper sharedDateTimeHelper];
    cell.detailTextLabel.text = [dateTimeHelper mediumStringForDate:note.date];

    cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;


    return cell;
}


- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{

    //NSLog(@"heightForRowAtIndexPath: Section %d Row %d", indexPath.section, indexPath.row);
    UITableViewCell *cell = [self tableView: self.tableView cellForRowAtIndexPath: indexPath];
    NSString *note = cell.textLabel.text;
    UIFont *font = [UIFont fontWithName:@"Helvetica" size:14.0];
    CGSize constraintSize = CGSizeMake(280.0f, MAXFLOAT);
    CGSize bounds = [note sizeWithFont:font constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];
    return (CGFloat) cell.bounds.size.height + bounds.height;

}

答案 9 :(得分:0)

当我一遍又一遍地搜索这个话题时,最后我想到了这个逻辑。一个简单的代码,但可能效率不高,但到目前为止,它是我能找到的最好的代码。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  NSDictionary * Object=[[NSDictionary alloc]init];
  Object=[Rentals objectAtIndex:indexPath.row];
  static NSString *CellIdentifier = @"RentalCell";
  RentalCell *cell = (RentalCell *)[tableView
                                  dequeueReusableCellWithIdentifier:CellIdentifier];
  if (cell == nil)
  {
      cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
  }
   NSString* temp=[Object objectForKey:@"desc"];
   int lines= (temp.length/51)+1;
   //so maybe here, i count how many characters that fit in one line in this case 51
   CGRect correctSize=CGRectMake(cell.infoLabel.frame.origin.x, cell.infoLabel.frame.origin.y,    cell.infoLabel.frame.size.width, (15*lines));
   //15 (for new line height)
   [cell.infoLabel setFrame:correctSize];
   //manage your cell here
}

这是代码的其余部分

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{

    NSDictionary * Object=[[NSDictionary alloc]init];
    Object=[Rentals objectAtIndex:indexPath.row];
    static NSString *CellIdentifier = @"RentalCell";
    RentalCell *cells = (RentalCell *)[tableView
                                  dequeueReusableCellWithIdentifier:CellIdentifier];
    NSString* temp=[Object objectForKey:@"desc"];
    int lines= temp.length/51;

    return (CGFloat) cells.bounds.size.height + (13*lines);
}