Подтвердить что ты не робот

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

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

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

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

Это не будет работать, потому что cellForRowAtIndexPath вызывается ПОСЛЕ heightForRowAtIndexPath.

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

Ответ 2

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

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

- (CGSize)sizeWithFont...

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

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

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

Ответ 3

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

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

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

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

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

Ответ 4

Вот как я вычисляю высоту ячейки на основе количества текста в 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

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

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

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

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

Ответ 6

Проблема с перемещением вычисления каждой ячейки в 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 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;
}

Ответ 7

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

Уточнение проблемы:

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

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

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

  • Решение 2: выполните первый проход "стандартного размера" для ячеек, результаты кеша, когда у вас есть высоты ячеек, а затем перезагрузите таблицу с помощью новых высот - Symmetric

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

Ответ 8

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

Ответ 9

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

}

Ответ 10

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

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