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

问题描述 投票:42回答:10

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

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

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

但这不起作用,因为cellForRowAtIndexPath被称为AFTER heightForRowAtIndexPath。

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

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

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

ios uitableview reloaddata heightforrowatindexpath
10个回答
60
投票

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

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

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

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

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

请参阅this one,也许还有- (CGSize)sizeWithFont...


0
投票

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

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

这是代码的其余部分

qazxswpoi

10
投票

所以,我认为你可以做到这一点,而不必一次创建你的细胞(正如你所说,这是浪费,也可能对大量细胞不切实际)。

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

docs

这是Apple Cocoa is My Girlfriend的链接。

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

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


5
投票

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

cellForRow:中,我然后调用该方法返回高度。

#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; } 中,我使用实际填充标签并调整大小的方法(您自己不会自己调整UITableView单元的大小)。

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

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


5
投票

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

PADDING

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


3
投票

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

基本上他们有一个派生自UITableViewController的类,它将@interface MyTableViewController : UITableViewController { NSMutableDictionary *heightForRowCache; BOOL reloadRequested; NSInteger maxElementBottom; NSInteger minElementTop; } 方法委托给TTTableCell类上的类方法。

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

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


3
投票

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

- (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:heightForRowAtIndexPath:

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

的tableView:的cellForRowAtIndexPath:

@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

2
投票

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

澄清问题:

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

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

  • 解决方案1:强制heightForRowAtIndexPath计算单元格的规格。 Massimo Cafaro 9月9日
  • 解决方案2:为单元格执行第一次“标准大小”,在具有单元格高度时缓存结果,然后使用新的高度重新加载表格 - 对称
  • 解决方案3:另一个有趣的答案似乎是涉及三个但是基于答案似乎没有在storyboard / xib中绘制的单元格会使这个“问题”更容易解决。

0
投票

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

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

}

0
投票

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

- (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
}
© www.soinside.com 2019 - 2024. All rights reserved.