Lógica de layout entre celdas en vistas de colección / tabla de iOS

¿Cuál es la forma idiomática de alinear porciones de celdas separadas en un UITableView o UICollectionView con una fila separada para los encabezados?

Por ejemplo, supongamos que quiero mostrar algo como esto:

 Name Number Bob 34587 Jane 32489 Barbara 23766 Montgomery 34892 

Los bits "Nombre" y "Número" estarían en una celda de encabezado de algún tipo. Creo que podría usar un encabezado de sección para esto (con una sola sección).

Cada celda debajo del encabezado debería dimensionarse de manera inteligente a su contenido, pero ese tamaño debería afectar el layout de todas las demás celdas.

¿Cómo se logra esto en iOS?

A pesar de que se le llama una "vista de tabla", en realidad no es una tabla la forma en que generalmente pensamos en tablas ( es decir , múltiples filas con múltiples columnas). Si realmente quieres varias columnas, depende de ti. Para alinearlos, debe hacer el trabajo de calcular el ancho necesario o elegir un ancho que sea lo suficientemente grande para todos los casos.

Creo que haría algo como esto:

 #define COLUMN_SPACE 10 - (UITableViewCell) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath { MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: @"cell"]; // Layout the cell NSString *name = [myDataStore nameForIndexPath: indexPath]; NSNumber *number = [myDataStore numberforIndexPath: indexPath]; CGRect frame = cell.nameLabel.frame; frame.size.width = [self nameColumnWidth]; cell.nameLabel.frame = frame; frame = cell.numberLabel.frame; frame.origin.x = cell.nameLabel.origin.x + cell.nameLabel.size.width + COLUMN_SPACE; frame.size.width = [self numberColumnWidth]; cell.numberLabel.frame = frame; cell.nameLabel.text = name; // You would probably want to use an NSNumberFormatter here cell.numberLabel.text = [NSString stringWithFormat: @"%d", number.intValue]; return cell; } - (CGFloat) nameColumnWidth { if( _nameColumnWidth <= 0.0 ) { _nameColumnWidth = 0.0; for( NSInteger s = 0; s < [myDataStore numberOfSections]; ++s ) { for( NSInteger r = 0; r < [myDataStore numberOfRowsInSection: s]; ++r ) { NSIndexPath *path = [NSIndexPath indexPathForRow: r inSection: s]; NSString *name = [myDataStore nameForIndexPath: indexPath]; CGFloat widthForName = [name widthNeeded]; if( widthForName > _nameColumnWidth ) _nameColumnWidth = widthForName; } } } return _nameColumnWidth; } - (CGFloat) numberColumnWidth { if( _numberColumnWidth <= 0.0 ) { _numberColumnWidth = 0.0; for( NSInteger s = 0; s < [myDataStore numberOfSections]; ++s ) { for( NSInteger r = 0; r < [myDataStore numberOfRowsInSection: s]; ++r ) { NSIndexPath *path = [NSIndexPath indexPathForRow: r inSection: s]; NSNumber *number = [myDataStore numberforIndexPath: indexPath]; // You would probably want to use an NSNumberFormatter here // (in fact you want to do the same is you do in cellForRowAtIndexPath) NSString *numberAsString = [NSString stringWithFormat: @"%d", number.intValue]; CGFloat widthForNumber = [numberAsString widthNeeded]; if( widthForNumber > _numberColumnWidth ) _numberColumnWidth = widthForNumber; } } } return _numberColumnWidth; } 

[self numberColumnWidth] el mismo [self nameColumnWidth] y [self numberColumnWidth] al generar la vista para el encabezado de la sección, o simplemente haría que la celda en la fila 0 de la sección tenga los encabezados Nombre y Número .

Para el ancho, extenderé NSString con esto:

 @implementation NSString (WithWidthNeeded) - (CGFloat) widthNeeded { // Either set the font you will use here, add it as a parameter, etc. UIFont *font = [UIFont systemFontOfSize: 18]; CGSize maximumLabelSize = CGSizeMake( 9999, font.lineHeight ); CGSize expectedLabelSize; if( [self respondsToSelector: @selector(boundingRectWithSize:options:attributes:context:)] ) { CGRect expectedLabelRect = [self boundingRectWithSize: maximumLabelSize options: NSStringDrawingUsesLineFragmentOrigin attributes: @{ NSFontAttributeName: font } context: nil]; expectedLabelRect = CGRectIntegral( expectedLabelRect ); expectedLabelSize = expectedLabelRect.size; } else { NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString: self attributes: @{ NSFontAttributeName: font }]; if( [attributedText respondsToSelector: @selector(boundingRectWithSize:options:context:)] ) { CGRect expectedLabelRect = [attributedText boundingRectWithSize: maximumLabelSize options: NSStringDrawingUsesLineFragmentOrigin context: nil]; expectedLabelRect = CGRectIntegral( expectedLabelRect ); expectedLabelSize = expectedLabelRect.size; } else { expectedLabelSize = [self sizeWithFont: font constrainedToSize: maximumLabelSize lineBreakMode: NSLineBreakByTruncatingTail]; } } return expectedLabelSize.width; } @end 

Cuando se cambian los datos, _nameColumnWidth y _numberColumnWidth se pueden cambiar a 0.0 y se volverán a calcular para que los anchos de columna se ajusten.

Una alternativa es realizar un seguimiento de los anchos máximos del nombre y el número cuando las celdas se distribuyen en cellForRowAtIndexPath . Si aumentan, activan las células visibles para volver a dibujarlas. Esto tendría un mejor performance, ya que no tendría que recorrer todo el set de datos para encontrar el nombre más largo y el mayor número . Sin embargo, esto daría como resultado que las columnas se desplacen mientras se desplaza la vista hacia abajo a través de los datos y se encuentran nombres más largos o más grandes , a less que el nombre más largo y el mayor número estén en la fila superior.

También sería posible hacer una especie de híbrido donde el nameColumnWidth numberColumnWidth y número numberColumnWidth estén configurados en el máximo visible (tipo de un máximo local), pero luego se inicia un subprocess de background para recorrer el rest de los datos y encontrar los máximos absolutos. Cuando se encuentran los máximos absolutos, se vuelven a cargar las celdas visibles. Luego, solo hay un cambio o cambio en el ancho de la columna. (El hilo de background podría no ser seguro si sizeWithFont: se llama porque está en UIKit . Con iOS 6 o 7 creo que estaría bien)

Una vez más, dado que una UITableView es realmente una UIColumnView le deja un poco de trabajo hacer varias columnas que pueden ajustar el ancho de su contenido.

Si te entiendo correctamente, te gustaría que la segunda columna se mueva lo más cerca posible de la primera columna sin frenar el contenido en la primera columna.

Como lo sé, esto no se usa comúnmente en iOS porque es mucho más fácil usar espacio de ancho fijo para ambas columnas y, por lo general, no hay necesidad de comprimirlos horizontalmente.

Sin embargo, si realmente necesitara lograr esto, entonces:

  1. Corrí en el ciclo a través del contenido de todas las celdas y determiné el tamaño del contenido izquierdo al llamar al método sizeWithFont:contrainedToSize: (si se usa antes de iOS7 o boundingRectWithSize:options:attributes:context: if on iOS7).
  2. Almacenaría el valor más grande en variable de instancia (por ejemplo _maxLeftContentWidth ).
  3. En mi -tableView:cellForRowAtIndexPath: establecía los frameworks para mi contenido de celda izquierda y contenido de celda derecha de acuerdo con mi variable de instancia almacenada ( _maxLeftContentWidth ).
  4. Cada vez que el contenido cambiaba para mis celdas, ejecutaba el método de cálculo de ancho de celda izquierdo y luego llamaba [self.tableView reloadData] para volver a dibujar las celdas.