Iphone – quando calcular heightForRowAtIndexPath para uma tableview quando cada altura de célula é dinâmica?

Eu já vi essa pergunta muitas vezes, mas surpreendentemente, eu não vi uma resposta consistente, então eu mesmo vou tentar:

Se você tem uma tableview contendo seu próprio UITableViewCells personalizado que contém UITextViews e UILabels cuja altura deve ser determinada em tempo de execução, como você deve determinar a altura para cada linha em heightForRowAtIndexPath?

A primeira ideia mais óbvia é calcular a altura de cada célula calculando e, em seguida, sumndo as alturas de cada visualização dentro da célula dentro de cellForRowAtIndexPath e armazenando a altura total final para recuperação posterior.

Isso não funcionará, no entanto, porque cellForRowAtIndexPath é chamado de AFTER heightForRowAtIndexPath.

A única coisa em que posso pensar é fazer todos os cálculos dentro de viewDidLoad, criar todos os UITableViewCells, calcular a altura das células e armazená-las em um campo personalizado dentro de sua subclass UITableViewCell e colocar cada célula em um NSMutableDictionary com o indexPath como a chave e, em seguida, basta recuperar a célula do dictionary usando o indexPath dentro de cellForRowAtIndexPath e heightForRowAtIndexPath, retornando o valor de altura personalizado ou o próprio object de célula.

Essa abordagem parece errada, porque não faz uso de dequeueReusableCellWithIdentifier, em vez disso eu estaria carregando todas as células de uma só vez em um dictionary em meu controlador, e os methods de delegates não estariam fazendo nada além de recuperar a célula correta do dictionary.

Eu não vejo outra maneira de fazer isso. Isso é uma má idéia? Se sim, qual é a maneira correta de fazer isso?

A forma como a Apple implementa o UITableView não é intuitiva para todos e é fácil entender mal o papel de heightForRowAtIndexPath: A intenção geral é que esse seja um método mais rápido e leve na memory, que pode ser chamado para cada linha da tabela com bastante frequência. Isso contrasta com cellForRowAtIndexPath: que geralmente é mais lento e cellForRowAtIndexPath: mais memory, mas só é chamado para as linhas que realmente precisam ser exibidas a qualquer momento.

Por que a Apple implementa isso assim? Parte da razão é que quase sempre é mais barato (ou pode ser mais barato se você codificar corretamente) para calcular a altura de uma linha do que para construir e preencher uma célula inteira. Dado que em muitas tabelas a altura de cada célula será idêntica, muitas vezes é muito mais barata. E outra parte do motivo é que o iOS precisa saber o tamanho da tabela inteira: isso permite criar as barras de rolagem e configurá-las em uma visualização de rolagem, etc.

Portanto, a menos que cada altura de célula seja a mesma, quando um UITableView é criado e sempre que você envia uma mensagem reloadData, a origem de dados recebe uma mensagem heightForRowAtIndexPath para cada célula. Então, se sua tabela tiver 30 células, essa mensagem será enviada 30 vezes. Digamos que apenas seis dessas 30 células estejam visíveis na canvas. Nesse caso, quando criado e quando você enviar uma mensagem reloadData, o UITableView enviará uma mensagem cellForRowAtIndexPath por linha visível, isto é, essa mensagem será enviada seis vezes.

Algumas pessoas às vezes ficam intrigadas sobre como calcular uma altura de célula sem criar as próprias visualizações . Mas geralmente isso é fácil de fazer.

Por exemplo, se as alturas das linhas variarem em tamanho, pois contêm quantidades variables ​​de texto, você poderá usar um dos methods sizeWithFont: na cadeia relevante para fazer os cálculos. Isso é mais rápido do que construir uma vista e depois medir o resultado. Observe que, se você alterar a altura de uma célula, precisará recarregar a tabela inteira (com reloadData – isso solicitará ao delegado todas as alturas, mas apenas solicitará células visíveis) OU recarregará seletivamente as linhas nas quais o tamanho alterado (que, da última vez que verifiquei, também chama heightForRowAtIndexPath: on ever row, mas também faz algum trabalho de rolagem para uma boa medida).

Veja esta questão e talvez também esta .

Então, eu acho que você pode fazer isso sem ter que criar suas células de uma só vez (o que, como você sugere, é um desperdício e provavelmente também impraticável para um grande número de células).

O UIKit adiciona alguns methods ao NSString, você pode tê-los perdido, pois eles não fazem parte da documentação principal do NSString. Os de interesse para você começam:

 - (CGSize)sizeWithFont... 

Aqui está o link para os documentos da Apple.

Em teoria, esses acréscimos NSString existem para esse problema exato: descobrir o tamanho que um bloco de texto ocupará sem precisar carregar a visão em si. Você presumivelmente já tem access ao texto para cada célula como parte da sua fonte de dados de visualização de tabela.

Eu digo “na teoria” porque se você está formatando em seu UITextView, sua milhagem pode variar com esta solução. Mas eu espero que você consiga pelo menos parte dele. Há um exemplo disso em Cocoa is My Girlfriend .

Uma abordagem que usei no passado é criar uma variável de class para manter uma única instância da célula que você usará na tabela (eu a chamo de protótipo de célula). Em seguida, na class de célula personalizada, tenho um método para preencher os dados e determinar a altura que a célula precisa ser. Observe que pode ser uma variante mais simples do método para realmente preencher os dados – em vez de resize um UILabel em uma célula, por exemplo, ele pode usar os methods de altura NSString para determinar a altura que o UILabel estaria na célula final e em seguida, use a altura total da célula (mais uma borda na parte inferior) e a colocação do UILabel para determinar a altura real. Você usa o protótipo de célula apenas para ter uma idéia de onde os elementos são colocados, assim você sabe o que significa quando um label vai ter 44 unidades de altura.

Em heightForRow: eu chamo esse método para retornar a altura.

Em cellForRow: eu uso o método que realmente preenche os labels e os redimensiona (você nunca redimensiona a célula UITableView).

Se você quiser ficar chique, você também pode armazenar em cache a altura de cada célula com base nos dados que você passar (por exemplo, pode ser apenas em um NSString, se isso é tudo o que determina a altura). Se você tem muitos dados que geralmente são o mesmo, pode fazer sentido ter um cache permanente em vez de apenas na memory.

Você também pode tentar estimar a contagem de linhas com base no caractere ou na contagem de palavras, mas, na minha experiência, isso nunca funciona – e quando dá errado, geralmente atrapalha uma célula e todas as células abaixo dela.

É assim que eu calculo a altura de uma célula com base na quantidade de texto em um 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; } 

Naturalmente, você precisaria ajustar o PADDING e outras variables ​​para atender às suas necessidades, mas isso define a altura da célula que possui um UITextView , com base na quantidade de texto fornecido. Portanto, se houver apenas três linhas de texto, a célula será bem curta, onde, como se houvesse 14 linhas de texto, a célula teria uma altura bastante grande.

A melhor implementação disso que eu vi é a forma como as classs do Three20 TTTableView fazem isso.

Basicamente, eles têm uma class derivada de UITableViewController que delega o método heightForRowAtIndexPath: para um método de class em uma class TTTableCell.

Essa class então retorna a altura certa, invariavelmente fazendo o mesmo tipo de cálculo de layout que você faz nos methods de desenho. Ao movê-lo para a class, ele evita escrever código que depende da instância da célula.

Não há realmente outra opção – por razões de desempenho, o framework não cria células antes de pedir suas alturas, e você não quer fazer isso também, se pode haver muitas linhas.

O problema com mover o cálculo de cada célula para tableView: heightForRowAtIndexPath: é que todas as células são recalculadas toda vez que reloadData é chamado. Muito lento, pelo menos para o meu aplicativo, onde pode haver 100 de linhas. Aqui está uma solução alternativa que usa uma altura de linha padrão e armazena em cache as alturas das linhas quando elas são calculadas. Quando uma altura muda ou é calculada pela primeira vez, um recarregamento de tabela é programado para informar a visualização de tabela das novas alturas. Isso significa que as linhas são exibidas duas vezes quando suas alturas são alteradas, mas isso é menor em comparação:

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

Muito boa pergunta: procurando mais informações sobre isso também.

Esclarecendo o problema:

  1. Altura para Row é chamada Antes do (cellForRowAtIndexPath)
  2. A maioria das pessoas calcula as informações do tipo de altura dentro do CELL (cellForRowAtIndexPath).

Algumas das soluções são surpreendentemente simples / efetivas:

  • solução 1: força o heightForRowAtIndexPath a calcular as especificações da célula. Massimo Cafaro 9 de setembro

  • solução 2: faça um “tamanho padrão” de primeira passagem para as células, armazene em cache os resultados quando você tiver alturas de células e, em seguida, recarregue a tabela usando as novas alturas – Symmetric

  • solução 3: a outra resposta interessante parece ser o envolvendo três20, mas com base na resposta parece que não há uma célula desenhada no storyboard / xib que torne esse “problema” muito mais fácil de resolver.

Eu fui com a idéia que propus originalmente, que parece funcionar bem, pelo que eu carrego todas as células personalizadas antes do tempo em viewDidLoad, armazená-las em um NSMutableDictionary com seu índice como a chave. Estou postando o código relevante e adoraria qualquer crítica ou opinião que alguém tenha sobre essa abordagem. Especificamente, não tenho certeza se existe algum problema de memory leaks com a maneira como estou criando o UITableViewCells a partir da ponta em viewDidLoad – já que não os libero.

 @interface RecentController : UIViewController  { 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 

Aqui está o que eu faço em um caso muito simples, uma célula contendo uma nota em um label. A nota em si é restrita a um tamanho máximo que estou impondo, portanto, uso um UILabel de várias linhas e calculo dinamicamente os oito corretos para cada célula, conforme mostrado no exemplo a seguir. Você pode lidar com um UITextView praticamente o mesmo.

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

Enquanto eu procurava mais e mais sobre esse assunto, finalmente essa lógica chegou ao meu pensamento. um código simples, mas talvez não seja eficiente o suficiente, mas até agora é o melhor que posso encontrar.

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

e aqui está o resto do código

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