Iphone - когда вычислять heightForRowAtIndexPath для таблицы, когда высота каждой ячейки является динамической? - PullRequest
42 голосов
/ 28 января 2011

Я видел, как этот вопрос задавали много раз, но поразительно, я не видел последовательного ответа, поэтому я попробую сам:

Если у вас есть табличное представление, содержащее ваши собственные пользовательские UITableViewCells, которые содержат UITextViewsи UILabels, высота которых должна быть определена во время выполнения, как вы должны определить высоту для каждой строки в heightForRowAtIndexPath?

Наиболее очевидная первая идея - вычислить высоту для каждой ячейки путем вычисления и суммирования высот.каждого представления внутри ячейки внутри cellForRowAtIndexPath и сохраните эту итоговую общую высоту для последующего извлечения.

Однако это не будет работать, потому что cellForRowAtIndexPath называется AFTER heightForRowAtIndexPath.

Единственное, о чем я могу думатьэто сделать все вычисления внутри viewDidLoad, затем создать все UITableViewCells, вычислить высоту ячеек и сохранить ее в настраиваемом поле внутри вашего подкласса UITableViewCell и поместить каждую ячейку в NSMutableDic.с ключом indexPath в качестве ключа, а затем просто извлекают ячейку из словаря, используя indexPath внутри cellForRowAtIndexPath и heightForRowAtIndexPath, возвращая либо собственное значение высоты, либо сам объект ячейки.

Хотя этот подход кажется неправильным, посколькуон не использует dequeueReusableCellWithIdentifier, вместо этого я загружал бы все ячейки одновременно в словарь в моем контроллере, и методы делегата не делали бы ничего, кроме извлечения правильной ячейки из словаря.

Iне вижу другого способа сделать это все же.Это плохая идея - если так, как правильно это сделать?

Ответы [ 10 ]

60 голосов
/ 09 сентября 2011

То, как Apple реализует UITableView, не является интуитивно понятным для всех, и легко понять значение heightForRowAtIndexPath:. Общее намерение состоит в том, что это более быстрый и легкий метод памяти, который можно вызывать для каждой строки в таблице довольно часто. Это отличается от cellForRowAtIndexPath:, который часто медленнее и требует больше памяти, но вызывается только для строк, которые действительно должны отображаться в любой момент времени.

Почему Apple реализует это так? Одной из причин является то, что почти всегда дешевле (или может быть дешевле, если вы правильно его кодируете) вычислить высоту строки, чем построить и заполнить целую ячейку. Учитывая, что во многих таблицах высота каждой ячейки будет одинаковой, она зачастую значительно дешевле. И другая часть причины заключается в том, что iOS необходимо знать размер всей таблицы: это позволяет ей создавать полосы прокрутки и настраивать их в представлении прокрутки и т. Д.

Таким образом, если высота каждой ячейки не одинакова, то при создании UITableView и при отправке ему сообщения reloadData источнику данных отправляется одно сообщение heightForRowAtIndexPath для каждой ячейки. Так что если в вашей таблице 30 ячеек, это сообщение будет отправлено 30 раз. Скажем, только шесть из этих 30 ячеек видны на экране. В этом случае при создании и при отправке ему сообщения reloadData UITableView отправит одно сообщение cellForRowAtIndexPath на видимую строку, то есть это сообщение будет отправлено шесть раз.

Некоторые люди иногда ломают голову над тем, как рассчитать высоту ячейки, не создавая сами представления . Но обычно это легко сделать.

Например, если высота строк различается по размеру, поскольку в них содержится различное количество текста, вы можете использовать один из методов sizeWithFont: в соответствующей строке для выполнения вычислений. Это быстрее, чем создание представления и последующее измерение результата. Обратите внимание, что если вы измените высоту ячейки, вам нужно будет либо перезагрузить всю таблицу (с помощью reloadData - это будет запрашивать у делегата каждую высоту, но только запрашивать видимые ячейки) ИЛИ выборочно перезагружать строки, где размер имеет изменен (который, в прошлый раз, когда я проверял, также вызывает heightForRowAtIndexPath: в каждой строке , но также выполняет некоторую прокрутку для хорошей меры).

См. этот вопрос и, возможно, также этот .

10 голосов
/ 28 января 2011

Итак, я думаю , что вы можете сделать это, не создавая сразу все ваши клетки (что, как вы предполагаете, расточительно и, вероятно, нецелесообразно для большого количества клеток).

UIKit добавляет пару методов к NSString, вы, возможно, пропустили их, поскольку они не являются частью основной документации NSString.Начнем с интересующих вас вопросов:

- (CGSize)sizeWithFont...

Вот ссылка на Apple docs .

Теоретически эти дополнения NSString существуют для этой конкретной задачи:чтобы определить размер, который блок текста будет занимать без необходимости загружать само представление.Предположительно, у вас уже есть доступ к тексту для каждой ячейки как части источника данных табличного представления.

Я говорю «в теории», потому что если вы выполняете форматирование в вашем UITextView, ваш пробег может варьироваться в зависимости от этого решения.Но я надеюсь, что это поможет вам хотя бы частично пройти туда.Вот пример этого на Какао - Моя Подруга .

5 голосов
/ 08 сентября 2011

Вот как я вычисляю высоту ячейки на основе объема текста в 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 строк, высота ячейки была довольно большой.

5 голосов
/ 28 января 2011

Подход, который я использовал в прошлом, заключается в создании переменной класса для хранения одного экземпляра ячейки, которую вы собираетесь использовать в таблице (я называю это ячейкой-прототипом). Затем в пользовательском классе ячеек у меня есть метод для заполнения данных и определения высоты, которой должна быть ячейка. Обратите внимание, что это может быть более простой вариант метода для реального заполнения данных - вместо фактического изменения размера UILabel в ячейке, например, он может просто использовать методы высоты NSString, чтобы определить, насколько высоким будет UILabel в последней ячейке, и затем используйте общую высоту ячейки (плюс границу снизу) и размещение UILabel, чтобы определить реальную высоту. Вы используете ячейку прототипа просто для того, чтобы получить представление о том, где расположены элементы, чтобы вы знали, что это значит, когда метка будет иметь высоту 44 единицы.

В heightForRow: Затем я вызываю этот метод для возврата высоты.

В cellForRow: Я использую метод, который фактически заполняет метки и изменяет их размер (вы никогда не изменяете размер ячейки UITableView самостоятельно).

Если вы хотите получить фантазию, вы также можете кэшировать высоту для каждой ячейки на основе данных, которые вы передаете (например, это может быть только одна строка NSString, если это все, что определяет высоту). Если у вас много данных, которые часто совпадают, может иметь смысл иметь постоянный кеш вместо просто в памяти.

Вы также можете попытаться оценить количество строк на основе количества символов или слов, но, по моему опыту, это никогда не срабатывает - и когда оно идет не так, как правило, портит ячейку и все ячейки под ней.

3 голосов
/ 07 февраля 2012

Проблема с перемещением вычисления каждой ячейки в tableView: heightForRowAtIndexPath: заключается в том, что все ячейки затем пересчитываются при каждом вызове reloadData. Слишком медленно, по крайней мере, для моего приложения, где могут быть сотни строк. Вот альтернативное решение, которое использует высоту строки по умолчанию и кэширует высоту строк при их вычислении. Когда высота изменяется или рассчитывается впервые, запланирована перезагрузка таблицы, чтобы сообщить табличному представлению о новых высотах. Это означает, что строки отображаются дважды при изменении их высоты, но это незначительно по сравнению:

@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;
}
3 голосов
/ 08 сентября 2011

Лучшая реализация, которую я видел, - это то, как классы Three20 TTTableView делают это.

В основном у них есть класс, производный от UITableViewController, который делегирует метод heightForRowAtIndexPath: методу класса в классе TTTableCell.

Этот класс затем возвращает правильную высоту, неизменно выполняя ту же сортировкурасчетов макета, как вы делаете в методах рисования.Перемещая его в класс, он избегает написания кода, который зависит от экземпляра ячейки.

Другого варианта на самом деле нет - по соображениям производительности среда не будет создавать ячейки, прежде чем запрашивать их высоту, а вы этого не делаетеЯ действительно хочу это сделать, если строк может быть много.

2 голосов
/ 04 декабря 2012

Действительно хороший вопрос: ищите более глубокое понимание этого вопроса.

Пояснение проблемы:

  1. Высота для строки вызывается перед (cellForRowAtIndexPath)
  2. Большинство людей вычисляют информацию о типе высоты в CELL (cellForRowAtIndexPath).

Некоторые решения удивительно просты / эффективны:

  • Решение 1: заставить heightForRowAtIndexPath вычислить спецификации ячейки.Массимо Кафаро, сентябрь 9

  • решение 2: сначала выполнить «стандартный размер» для ячеек, кэшировать результаты, когда у вас есть высота ячеек, затем перезагрузить таблицу, используя новые высоты - Симметричный

  • решение 3: другой интересный ответ, кажется, включает в себя три20, но, исходя из ответа, кажется, что в раскадровке / XIB не нарисована ячейка, которая бы создала эту «проблему»"гораздо проще решить.

0 голосов
/ 11 декабря 2012

Когда я снова и снова искал эту тему, наконец, эта логика пришла мне в голову.простой код, но, возможно, недостаточно эффективный, но пока это лучшее, что я могу найти.

- (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);
}
0 голосов
/ 09 сентября 2011

Вот что я делаю в очень простом случае - ячейка, содержащая заметку в ярлыке.Сама заметка ограничена максимальной длиной, которую я налагаю, поэтому я использую многострочную 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;

}
0 голосов
/ 29 января 2011

Я пошел с идеей, которую я первоначально предложил, которая, кажется, работает нормально, поэтому я загружаю все пользовательские ячейки заранее в viewDidLoad, сохраняю их в NSMutableDictionary с их индексом в качестве ключа.Я публикую соответствующий код и буду любить любые критические замечания или мнения по этому поводу.В частности, я не уверен, есть ли какая-либо проблема с утечкой памяти в способе создания UITableViewCells из кончика в 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
...