42

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

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

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

然而这将不起作用,因为 cellForRowAtIndexPath 在 heightForRowAtIndexPath 之后被调用。

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

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

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

4

10 回答 10

61

Apple 实现 UITableView 的方式对每个人来说都不是很直观,很容易误解heightForRowAtIndexPath:. 总体意图是,这是一种更快、更轻的内存方法,可以非常频繁地为表中的每一行调用。这与之形成对比,cellForRowAtIndexPath:后者通常更慢且内存更密集,但仅在任何给定时间实际需要显示的行才会调用。

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

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

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

例如,如果您的行高因包含不同数量的文本而大小不同,则可以使用sizeWithFont:相关字符串上的一种方法来进行计算。这比构建视图然后测量结果要快。请注意,如果您更改单元格的高度,您将需要重新加载整个表格(使用 reloadData - 这将询问代表每个高度,但只询问可见单元格)或有选择地重新加载具有大小的行改变了(上次我检查时,它也调用heightForRowAtIndexPath:了每一行,但也做了一些滚动工作以取得良好的效果)。

看到这个问题,也许还有这个

于 2011-09-09T15:03:45.967 回答
10

所以,我认为你可以做到这一点,而不必一次创建你的单元格(正如你所建议的那样,这很浪费,而且对于大量单元格来说也可能不切实际)。

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

- (CGSize)sizeWithFont...

这是 Apple文档的链接。

理论上,这些 NSString 的添加是为了解决这个确切的问题:在不需要加载视图本身的情况下计算出一块文本将占用的大小。作为表格视图数据源的一部分,您可能已经可以访问每个单元格的文本。

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

于 2011-01-28T01:00:32.953 回答
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会根据提供的文本量设置其中包含 a 的单元格的高度。因此,如果只有 3 行文本,则单元格相当短,而如果有 14 行文本,则单元格的高度相当大。

于 2011-09-08T07:37:24.183 回答
5

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

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

cellForRow:我使用实际填充标签并调整它们大小的方法(你永远不会自己调整 UITableView 单元格的大小)。

如果你想变得花哨,你还可以根据你传入的数据缓存每个单元格的高度(例如,如果这就是确定高度的全部,它可能只是在一个 NSString 上)。如果您有很多通常相同的数据,那么拥有永久缓存而不是仅在内存中可能是有意义的。

您也可以尝试根据字符数或字数估算行数,但根据我的经验,这永远行不通——当它出错时,它通常会弄乱一个单元格及其下面的所有单元格。

于 2011-01-28T01:51:45.663 回答
3

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

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

表视图: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;
}

表视图: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;
}
于 2012-02-07T00:41:27.093 回答
3

我见过的最好的实现是 Three20 TTTableView 类的做法。

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

然后该类返回正确的高度,总是通过与您在绘图方法中所做的相同类型的布局计算。通过将其移动到类中,它避免了编写依赖于单元实例的代码。

确实没有其他选择 - 出于性能原因,框架不会在询问它们的高度之前创建单元格,如果可能有很多行,你也不想这样做。

于 2011-09-07T20:06:55.797 回答
2

非常好的问题:也在寻找对此的更多见解。

澄清问题:

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

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

  • 解决方案 1:强制 heightForRowAtIndexPath 计算单元格的规格。马西莫·卡法罗 9 月 9 日

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

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

于 2012-12-04T04:59:18.377 回答
0

这是我在非常简单的情况下所做的,一个包含标签中的注释的单元格。注释本身被限制为我施加的最大长度,因此我使用多行 UILabel 并为每个单元格动态计算正确的八个,如以下示例所示。您可以处理几乎相同的 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;

}
于 2011-09-09T10:44:25.053 回答
0

我采用了我最初提出的想法,这似乎工作正常,我提前在 viewDidLoad 中加载所有自定义单元格,将它们存储在 NSMutableDictionary 中,并将它们的索引作为键。我正在发布相关代码,并且希望任何人对这种方法有任何批评或意见。具体来说,我不确定从 viewDidLoad 中的笔尖创建 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
于 2011-01-29T03:41:45.207 回答
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);
}
于 2012-12-11T07:05:20.153 回答