tutorial iphone cocoa-touch

tutorial - Iphone: ¿cuándo se debe calcular heightForRowAtIndexPath para una vista de tabla cuando la altura de cada celda es dinámica?



cocoa touch tutorial (10)

He visto esta pregunta muchas veces pero, sorprendentemente, no he visto una respuesta coherente, así que lo intentaré yo mismo:

Si tiene una vista de tabla que contiene sus propias UITableViewCells personalizadas que contienen UITextViews y UILabels cuya altura debe determinarse en tiempo de ejecución, ¿cómo se supone que debe determinar la altura de cada fila en heightForRowAtIndexPath?

La primera idea más obvia es calcular la altura de cada celda calculando y luego sumando las alturas de cada vista dentro de la celda dentro de cellForRowAtIndexPath, y almacenar esa altura total final para su posterior recuperación.

Sin embargo, esto no funcionará porque cellForRowAtIndexPath se llama AFTER heightForRowAtIndexPath.

Lo único que se me ocurre es hacer todos los cálculos dentro de viewDidLoad, crear todas las UITableViewCells, calcular la altura de las celdas y almacenarlas en un campo personalizado dentro de su subclase UITableViewCell, y colocar cada celda en un NSMutableDictionary con indexPath como la clave, y luego simplemente recupere la celda del diccionario usando indexPath dentro de cellForRowAtIndexPath y heightForRowAtIndexPath, devolviendo el valor de altura personalizado o el objeto de celda en sí.

Sin embargo, este enfoque parece incorrecto porque no utiliza dequeueReusableCellWithIdentifier, en su lugar, estaría cargando todas las celdas de una vez en un diccionario en mi controlador, y los métodos de delegado no harían nada más que recuperar la celda correcta del diccionario.

Aunque no veo otra forma de hacerlo. ¿Es esta una mala idea? Si es así, ¿cuál es la forma correcta de hacerlo?


Así es como calculo la altura de una celda en función de la cantidad de texto en un 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; }

Por supuesto, necesitaría ajustar el PADDING y otras variables para que se ajusten a sus necesidades, pero esto establece la altura de la celda que contiene un UITextView en función de la cantidad de texto suministrado. por lo tanto, si solo hay 3 líneas de texto, la celda es bastante corta, donde, como si hubiera 14 líneas de texto, la celda es bastante grande en altura.


El problema con mover el cálculo de cada celda a tableView: heightForRowAtIndexPath: es que todas las celdas se recalculan cada vez que se llama a reloadData. Demasiado lento, al menos para mi aplicación donde puede haber cientos de filas. Aquí hay una solución alternativa que utiliza una altura de fila predeterminada y almacena en caché las alturas de las filas cuando se calculan. Cuando una altura cambia, o se calcula por primera vez, se programa una recarga de tabla para informar a la vista de tabla de las nuevas alturas. Esto significa que las filas se muestran dos veces cuando cambian sus alturas, pero eso es menor en comparación:

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


Esto es lo que hago en un caso muy simple, una celda que contiene una nota contenida en una etiqueta. La nota en sí misma está restringida a una longitud máxima que estoy imponiendo, así que uso una UILabel multilínea y calculo dinámicamente los ocho correctos para cada celda como se muestra en el siguiente ejemplo. Puedes lidiar con un UITextView más o menos igual.

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


Fui con la idea que propuse originalmente, que parece funcionar bien, por lo que cargo todas las celdas personalizadas antes de tiempo en viewDidLoad, las almaceno en un NSMutableDictionary con su índice como la clave. Estoy publicando el código relevante y me encantaría cualquier crítica u opinión que alguien tenga sobre este enfoque. Específicamente, no estoy seguro de si hay algún problema de pérdida de memoria en la forma en que estoy creando las UITableViewCells desde la punta en viewDidLoad, ya que no las libero.

@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


La forma en que Apple implementa UITableView no es intuitiva para todos y es fácil malinterpretar el rol de heightForRowAtIndexPath: La intención general es que este es un método más rápido y ligero en la memoria que se puede llamar para cada fila de la tabla con bastante frecuencia. Esto contrasta con cellForRowAtIndexPath: que a menudo es más lento y requiere más memoria, pero solo se llama para las filas que realmente se deben mostrar en un momento dado.

¿Por qué Apple lo implementa así? Parte de la razón es que casi siempre es más barato (o puede ser más barato si lo codifica correctamente) calcular la altura de una fila que construir y poblar una celda completa. Dado que en muchas tablas la altura de cada celda será idéntica, a menudo es mucho más barata. Y otra parte de la razón es que iOS necesita saber el tamaño de toda la tabla: esto le permite crear las barras de desplazamiento y configurarlo en una vista de desplazamiento, etc.

Entonces, a menos que la altura de cada celda sea la misma, cuando se crea un UITableView y cada vez que le envía un mensaje reloadData, la fuente de datos recibe un mensaje heightForRowAtIndexPath para cada celda. Entonces, si su mesa tiene 30 celdas, ese mensaje se envía 30 veces Digamos que solo seis de esas 30 celdas son visibles en la pantalla. En ese caso, cuando se crea y cuando le envía un mensaje reloadData, el UITableView enviará un mensaje cellForRowAtIndexPath por fila visible, es decir, ese mensaje se envía seis veces.

Algunas personas a veces se sorprenden acerca de cómo calcular la altura de una celda sin crear las vistas por sí mismas . Pero generalmente esto es fácil de hacer.

Por ejemplo, si las alturas de las filas varían en tamaño debido a que contienen cantidades variables de texto, puede usar uno de los métodos sizeWithFont: en la cadena correspondiente para realizar los cálculos. Esto es más rápido que construir una vista y luego medir el resultado. Tenga en cuenta que si cambia la altura de una celda, tendrá que volver a cargar toda la tabla (con reloadData - esto le pedirá al delegado cada altura, pero solo solicitar celdas visibles) O recargar selectivamente las filas donde el tamaño tiene cambiado (que, la última vez que lo verifiqué, también llama heightForRowAtIndexPath: en cada fila, pero también hace un poco de trabajo de desplazamiento en buena medida).

Vea esta pregunta y quizás también esta .


La mejor implementación de esto que he visto es la forma en que las clases de Three20 TTTableView lo hacen.

Básicamente, tienen una clase derivada de UITableViewController que delega el método heightForRowAtIndexPath: a un método de clase en una clase TTTableCell.

Esa clase luego devuelve la altura correcta, invariablemente haciendo el mismo tipo de cálculos de diseño que haces en los métodos de dibujo. Al moverlo a la clase, evita escribir código que depende de la instancia de la celda.

Realmente no hay otra opción, por razones de rendimiento, el marco no creará celdas antes de preguntar por sus alturas, y tampoco querrá hacerlo si hay muchas filas.


Mientras buscaba una y otra vez sobre este tema, finalmente esta lógica llegó a mi pensamiento. un código simple, pero quizás no lo suficientemente eficiente, pero hasta ahora es lo mejor que puedo 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 }

Y aquí está el resto del 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); }


Muy buena pregunta: buscando más información sobre esto también.

Aclarando el tema:

  1. Altura de fila se llama antes de (cellForRowAtIndexPath)
  2. La mayoría de las personas calculan la información de tipo de altura dentro de CELL (cellForRowAtIndexPath).

Algunas de las soluciones son sorprendentemente simples / efectivas:

  • solución 1: obligue a la heightForRowAtIndexPath a calcular las especificaciones de la celda. Massimo Cafaro 9 de septiembre

  • solución 2: realice un primer "tamaño estándar" para las celdas, almacene en caché los resultados cuando tenga alturas de celdas y luego vuelva a cargar la tabla con las nuevas alturas: simétrico

  • solución 3: la otra respuesta interesante parece ser la de three20, pero según la respuesta, parece que no hay una celda dibujada en el guión gráfico / xib que haría que este "problema" sea mucho más fácil de resolver.


Por lo tanto, creo que puede hacer esto sin tener que crear todas sus celdas a la vez (lo que, como sugiere, es un desperdicio y probablemente tampoco sea práctico para una gran cantidad de celdas).

UIKit agrega un par de métodos a NSString, es posible que los haya perdido ya que no forman parte de la documentación principal de NSString. Los de interés para ustedes comienzan:

- (CGSize)sizeWithFont...

Aquí está el enlace a los docs Apple.

En teoría, estas adiciones NSString existen para este problema exacto: para averiguar el tamaño que tomará un bloque de texto sin necesidad de cargar la vista en sí. Probablemente ya tenga acceso al texto de cada celda como parte de su fuente de datos de vista de tabla.

Digo "en teoría" porque si está realizando el formateo en su UITextView, su kilometraje puede variar con esta solución. Pero espero que te lleve al menos parte de allí. Hay un ejemplo de esto en Cocoa es My Girlfriend .


Un enfoque que he usado en el pasado es crear una variable de clase para contener una sola instancia de la celda que va a usar en la tabla (lo llamo una celda prototipo). Luego, en la clase de celda personalizada, tengo un método para rellenar los datos y determinar la altura que debe tener la celda. Tenga en cuenta que puede ser una variante más simple del método para llenar realmente los datos; en lugar de cambiar el tamaño de una UILabel en una celda, por ejemplo, puede usar los métodos de altura NSString para determinar qué tan alta sería la UILabel en la celda final y luego use la altura total de la celda (más un borde en la parte inferior) y la ubicación de UILabel para determinar la altura real. Usted usa la célula prototipo solo para tener una idea de dónde se colocan los elementos para que sepa qué significa cuando una etiqueta tendrá 44 unidades de alto.

En heightForRow: luego llamo a ese método para devolver la altura.

En cellForRow: uso el método que realmente rellena las etiquetas y las cambia de tamaño (usted nunca cambia el tamaño de la celda UITableView).

Si desea obtener fantasía, también puede almacenar en caché la altura de cada celda según los datos que pase (por ejemplo, podría estar en una NSString si eso es todo lo que determina la altura). Si tiene una gran cantidad de datos que a menudo son los mismos, puede tener sentido tener un caché permanente en lugar de solo en la memoria.

También puede intentar estimar el recuento de líneas basado en el recuento de caracteres o palabras, pero en mi experiencia eso nunca funciona, y cuando falla, generalmente desordena una celda y todas las celdas debajo de ella.