iphone – 何时计算heightForRowAtIndexPath为tableview时,每个单元格的高度是dynamic的?

我曾多次看到这个问题,但令人震惊的是,我还没有看到一致的答案,所以我会自己试一下:

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

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

这将无法正常工作,因为cellForRowAtIndexPath被称为AFTER heightForRowAtIndexPath。

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

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

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

苹果公司实现UITableView的方式对每个人都不直观,很容易误解heightForRowAtIndexPath:的作用。 总的目的是这是一个更快,更简单的内存方法,可以频繁地调用表中的每一行。 这与cellForRowAtIndexPath:形成了鲜明的对比cellForRowAtIndexPath:这通常比较慢,内存更密集,但是只有在任何给定的时间才需要显示的行才会被调用。

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

因此,除非每个单元格的高度相同,否则当创build一个UITableView时,每当发送一个reloadData消息时,数据源就会为每个单元格发送一个heightForRowAtIndexPath消息。 所以如果你的表有30个单元格,那么这个消息被发送了30次。 说30个单元格中只有6个在屏幕上可见。 在这种情况下,当创build并发送reloadData消息时,UITableView将在每个可见行发送一个cellForRowAtIndexPath消息,即该消息被发送六次。

有些人不知道如何计算单元格高度而不创build视图本身 。 但通常这很容易做到。

例如,如果您的行高因大小不同而不同,您可以使用相关string上的一个sizeWithFont:方法来进行计算。 这比build立一个视图然后测量结果更快。 请注意,如果您更改单元格的高度,则需要重新加载整个表格(使用reloadData – 这将询问代表每个高度,但只询问可见单元格),或者有select地重新加载大小已满的行(最后一次我检查,也调用heightForRowAtIndexPath:在有史以来, 也做了一些滚动工作的好措施)。

看到这个问题 ,也许也是这个 。

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

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

 - (CGSize)sizeWithFont... 

这里是苹果文档的链接。

理论上,这些NSString增加存在这个确切的问题:找出一个文本块将占用的大小, 而不需要加载视图本身。 你大概已经有权访问每个单元格的文本,作为你的表格视图数据源的一部分。

我说“理论上”,因为如果你在UITextView中进行格式化,你的里程可能会随着这个解决scheme而变化。 但是我希望这会让你至less有一部分的方式。 cocoa是我的女朋友就是一个例子。

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

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

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

如果你想获得幻想,你也可以根据你传入的数据来caching每个单元格的高度(例如,如果这是所有决定高度的话,它可能只是在一个NSString上)。 如果有大量的数据通常是相同的,那么永久性caching而不是内存就是有意义的。

你也可以尝试根据字符或者字数来估计行数,但是根据我从来没有用过的经验 – 当它出错的时候,通常会把一个单元格和它下面的所有单元格混淆起来。

这是我如何根据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和其他variables以适合你的需要,但是这将根据提供的文本量来设置包含UITextView的单元格的高度。 所以如果只有三行文本,那么这个单元就相当短,就好像有十四行文本一样,这个单元的高度就比较大。

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

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

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

实际上没有其他的select – 出于性能方面的原因,框架在请求高度之前不会创build单元格,而且如果可能有很多行,则不会真的想这样做。

将每个单元格的计算移动到tableView:heightForRowAtIndexPath:的问题在于,每次调用reloadData时,都将重新计算所有单元格。 太慢了,至less对于我的应用程序可能有100多行。 以下是使用默认行高的替代解决scheme,并在计算行高时进行高速caching。 当高度改变或者首先计算时,计划表重新加载以通知表格视图新的高度。 这确实意味着行数在高度变化时会显示两次,但相比之下,

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

真的很好的问题:在这方面寻找更多的见解。

澄清问题:

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

一些解决scheme出奇的简单/有效:

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

  • 解决scheme2:为单元格执行第一遍“标准大小”,当您确实拥有单元高度时caching结果,然后使用新高度重新载入表格 – 对称

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

我最初提出的想法,似乎工作正常,我提前加载所有的自定义单元格在viewDidLoad中,将它们存储在一个NSMutableDictionary与他们的索引作为关键。 我发布了相关的代码,并会喜欢任何人对此方法的批评或意见。 具体来说,我不知道是否有任何内存泄漏问题的方式,我创build的UIDableViewCells从笔尖viewDidLoad – 因为我不释放它们。

 @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 

这就是我在非常简单的情况下做的事情,一个包含一个标签的单元格。 音符本身被限制在我正在施加的最大长度,所以我使用多行UILabel,并且dynamic地为每个单元格计算正确的八,如以下示例所示。 你可以处理一个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; } 

当我一遍又一遍地search这个话题时,终于有了这个逻辑。 一个简单的代码,但可能不够高效,但到目前为止,这是我能find的最好的。

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