NSTableView basado en vistas con filas que tienen alturas dinámicas

84

Tengo una aplicación con una vista basada NSTableViewen ella. Dentro de esta vista de tabla, tengo filas que tienen celdas que tienen contenido que consta de varias filas NSTextFieldcon ajuste de palabras habilitado. Dependiendo del contenido textual del NSTextField, el tamaño de las filas necesarias para mostrar la celda variará.

Sé que puedo implementar el NSTableViewDelegatemétodo, tableView:heightOfRow:para devolver la altura, pero la altura se determinará en función del ajuste de palabras utilizado en NSTextField. La palabra envoltura de NSTextFieldse basa de manera similar en qué tan ancho NSTextFieldes ... que está determinado por el ancho de NSTableView.

Así que… supongo que mi pregunta es… ¿cuál es un buen patrón de diseño para esto? Parece que todo lo que intento termina siendo un lío complicado. Dado que TableView requiere conocimiento de la altura de las celdas para distribuirlas ... y el NSTextFieldconocimiento de las necesidades de su diseño para determinar el ajuste de palabras ... y la celda necesita conocimiento del ajuste de palabras para determinar su altura ... es un lío circular ... y me está volviendo loco .

Sugerencias

Si importa, el resultado final también tendrá elementos editables NSTextFieldsque cambiarán de tamaño para ajustarse al texto dentro de ellos. Ya tengo esto funcionando en el nivel de vista, pero la vista de tabla aún no ajusta las alturas de las celdas. Me imagino que una vez que resuelva el problema noteHeightOfRowsWithIndexesChangedde la altura, usaré el método - para informar a la vista de la mesa que la altura cambió ... pero aún así le pedirá al delegado la altura ... de ahí, mi problema.

¡Gracias por adelantado!

Jiva DeVoe
fuente
Si tanto el ancho como el alto de la vista pueden cambiar con su contenido, entonces definirlos es complicado porque tiene que iterar (pero luego puede almacenar en caché). Pero su caso parece tener una altura variable para un ancho constante. Si se conoce el ancho (es decir, basado en el ancho de la ventana o vista), entonces nada aquí es complicado. Me falta por qué el ancho es variable.
Rob Napier
Solo la altura puede cambiar con el contenido. Y ya resolví ese problema. Tengo una subclase NSTextField que ajusta su altura automáticamente. El problema es que el delegado de la vista de tabla conozca ese ajuste, para que pueda actualizar la altura en consecuencia.
Jiva DeVoe
@Jiva: Me pregunto si ya lo has resuelto.
Joshua Nozzi
Estoy intentando hacer algo similar. Me gusta la idea de una subclase NSTextField que ajusta su propia altura. ¿Qué tal agregar una notificación (delegada) de cambio de altura a esa subclase y monitorear eso con alguna clase apropiada (fuente de datos, delegado de esquema, ...) para obtener información sobre el esquema?
John Velman
Una pregunta muy relacionada, con respuestas que me ayudaron más que las de aquí: NSTableView Row Height basado en NSStrings .
Ashley

Respuestas:

132

Este es el problema del huevo y la gallina. La tabla necesita conocer la altura de la fila porque eso determina dónde se ubicará una vista determinada. Pero desea que ya exista una vista para poder usarla para calcular la altura de la fila. Entonces, ¿qué viene primero?

La respuesta es mantener una vista adicional NSTableCellView(o cualquier vista que esté usando como su "vista de celda") alrededor solo para medir la altura de la vista. En el tableView:heightOfRow:método de delegado, acceder a su modelo de 'fila' y establecer el objectValuesobre NSTableCellView. Luego, establezca el ancho de la vista para que sea el ancho de su tabla y (como quiera hacerlo) calcule la altura requerida para esa vista. Devuelve ese valor.

¡No llame noteHeightOfRowsWithIndexesChanged:desde el método delegado tableView:heightOfRow:o viewForTableColumn:row:! Eso es malo y causará grandes problemas.

Para actualizar dinámicamente la altura, lo que debe hacer es responder al cambio de texto (a través del objetivo / acción) y volver a calcular la altura calculada de esa vista. Ahora, no cambie dinámicamente la NSTableCellViewaltura de (o cualquier vista que esté usando como su "vista de celda"). La tabla debe controlar el marco de esa vista, y lucharás contra la vista de tabla si intentas configurarla. En cambio, en su objetivo / acción para el campo de texto donde calculó la altura, llame noteHeightOfRowsWithIndexesChanged:, lo que permitirá que la tabla cambie el tamaño de esa fila individual. Suponiendo que haya configurado la máscara de tamaño automático en las subvistas (es decir, subvistas de NSTableCellView), ¡las cosas deberían cambiar de tamaño bien! Si no es así, primero trabaje en la máscara de cambio de tamaño de las subvistas para hacer las cosas bien con alturas de fila variables.

No olvides que se noteHeightOfRowsWithIndexesChanged:anima de forma predeterminada. Para que no sea animado:

[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0];
[tableView noteHeightOfRowsWithIndexesChanged:indexSet];
[NSAnimationContext endGrouping];

PD: Respondo más a las preguntas publicadas en los foros de desarrollo de Apple que al desbordamiento de pila.

PSS: escribí el NSTableView basado en vistas

Corbin Dunn
fuente
Gracias por la respuesta. No lo había visto originalmente ... Lo marqué como la respuesta correcta. Los foros de desarrollo son geniales, yo también los uso.
Jiva DeVoe
"calcula la altura requerida para esa vista. Devuelve ese valor". .. Ese es mi problema :(. Estoy usando tableView basado en vistas, ¡y no tengo idea de cómo hacerlo!
Mazyod
@corbin Gracias por la respuesta, esto es lo que tenía en mi aplicación. Sin embargo, encontré un problema con Mavericks. Ya que está claramente versado en este tema, ¿le importaría revisar mi pregunta? Aquí: stackoverflow.com/questions/20924141/…
Alex
1
@corbin, ¿hay una respuesta canónica para esto, pero usando el diseño automático y las restricciones, con NSTableViews? Existe una solución potencial con el diseño automático; preguntándose si eso es lo suficientemente bueno sin las API EstimadoHeightForRowAtIndexPath (que no está en Cocoa)
ZS
3
@ZS: Estoy usando Auto Layout e implementé la respuesta de Corbin. En tableView:heightOfRow:, configuro mi vista de celda de repuesto con los valores de la fila (solo tengo una columna) y regreso cellView.fittingSize.height. fittingSizees un método NSView que calcula el tamaño mínimo de la vista que satisface sus restricciones.
Peter Hosey
43

Esto se hizo mucho más fácil en macOS 10.13 con .usesAutomaticRowHeights. Los detalles están aquí: https://developer.apple.com/library/content/releasenotes/AppKit/RN-AppKit/#10_13 (En la sección titulada "NSTableView Automatic Row Heights").

Básicamente, simplemente seleccione su NSTableViewo NSOutlineViewen el editor de guión gráfico y seleccione esta opción en el Inspector de tamaño:

ingrese la descripción de la imagen aquí

Luego, configura las cosas en su NSTableCellViewpara tener restricciones superiores e inferiores en la celda y su celda cambiará de tamaño para ajustarse automáticamente. ¡No se requiere código!

Su aplicación ignorará las alturas especificadas en heightOfRow( NSTableView) y heightOfRowByItem( NSOutlineView). Puede ver qué alturas se calculan para sus filas de diseño automático con este método:

func outlineView(_ outlineView: NSOutlineView, didAdd rowView: NSTableRowView, forRow row: Int) {
  print(rowView.fittingSize.height)
}
Clifton Labrum
fuente
@tofutim: si funciona correctamente con celdas de texto envuelto si usa las restricciones correctas y cuando se asegura de que la configuración de Ancho preferido de cada NSTextField con altura dinámica, esté establecida en Automático. Consulte también stackoverflow.com/questions/46890144/…
Ely
Creo que @tofutim tiene razón, esto no funcionará si pones NSTextView en TableView, intenta archivarlo pero no funciona. Sin embargo, funciona para otros componentes, por ejemplo, NSImage ..
Albert
Usé una altura fija para una fila pero no me funciona
Ken Zira
2
Después de unos días de averiguar por qué no funciona para mí, descubrí: funciona SOLO cuando TableCellViewNO es editable. No importa está marcado en Atributos [Comportamiento: Editable] o Inspector de enlaces [Establecer editable condicionalmente: Sí]
Łukasz
Solo quiero enfatizar lo importante que es tener restricciones de altura reales en la celda de su tabla: la mía no, por lo que el tamaño de ajuste calculado de la celda fue 0.0 y se produjo la frustración.
green_knight
9

Basado en la respuesta de Corbin (por cierto, gracias por arrojar algo de luz sobre esto):

Swift 3, NSTableView basado en vistas con diseño automático para macOS 10.11 (y superior)

Mi configuración: tengo una NSTableCellViewque se presenta usando Auto-Layout. Contiene (además de otros elementos) una multilínea NSTextFieldque puede tener hasta 2 filas. Por lo tanto, la altura de la vista de celda completa depende de la altura de este campo de texto.

Actualizo le digo a la vista de tabla que actualice la altura en dos ocasiones:

1) Cuando la vista de tabla cambia de tamaño:

func tableViewColumnDidResize(_ notification: Notification) {
        let allIndexes = IndexSet(integersIn: 0..<tableView.numberOfRows)
        tableView.noteHeightOfRows(withIndexesChanged: allIndexes)
}

2) Cuando cambia el objeto del modelo de datos:

tableView.noteHeightOfRows(withIndexesChanged: changedIndexes)

Esto hará que la vista de la tabla solicite a su delegado la nueva altura de fila.

func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {

    // Get data object for this row
    let entity = dataChangesController.entities[row]

    // Receive the appropriate cell identifier for your model object
    let cellViewIdentifier = tableCellViewIdentifier(for: entity)

    // We use an implicitly unwrapped optional to crash if we can't create a new cell view
    var cellView: NSTableCellView!

    // Check if we already have a cell view for this identifier
    if let savedView = savedTableCellViews[cellViewIdentifier] {
        cellView = savedView
    }
    // If not, create and cache one
    else if let view = tableView.make(withIdentifier: cellViewIdentifier, owner: nil) as? NSTableCellView {
        savedTableCellViews[cellViewIdentifier] = view
        cellView = view
    }

    // Set data object
    if let entityHandler = cellView as? DataEntityHandler {
        entityHandler.update(with: entity)
    }

    // Layout
    cellView.bounds.size.width = tableView.bounds.size.width
    cellView.needsLayout = true
    cellView.layoutSubtreeIfNeeded()

    let height = cellView.fittingSize.height

    // Make sure we return at least the table view height
    return height > tableView.rowHeight ? height : tableView.rowHeight
}

Primero, necesitamos obtener nuestro objeto modelo para la fila ( entity) y el identificador de vista de celda apropiado. Luego verificamos si ya hemos creado una vista para este identificador. Para hacer eso, tenemos que mantener una lista con vistas de celda para cada identificador:

// We need to keep one cell view (per identifier) around
fileprivate var savedTableCellViews = [String : NSTableCellView]()

Si no se guarda ninguno, necesitamos crear (y almacenar en caché) uno nuevo. Actualizamos la vista de celda con nuestro objeto modelo y le decimos que vuelva a distribuir todo en función del ancho actual de la vista de tabla. La fittingSizealtura se puede utilizar como nueva altura.

JanApotheker
fuente
Esta es la respuesta perfecta. Si tiene varias columnas, querrá establecer límites al ancho de la columna, no al ancho de la tabla.
Vojto
Además, mientras lo configura cellView.bounds.size.width = tableView.bounds.size.width, es una buena idea restablecer la altura a un número bajo. Si planea reutilizar esta vista ficticia, establecerla en un número bajo "restablecerá" el tamaño de ajuste.
Vojto
7

Para cualquiera que desee más código, aquí está la solución completa que utilicé. Gracias Corbin Dunn por señalarme en la dirección correcta.

Necesitaba establecer la altura principalmente en relación con la altura NSTextViewde mi NSTableViewCell.

En mi subclase de, NSViewControllercreo temporalmente una nueva celda llamandooutlineView:viewForTableColumn:item:

- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
{
    NSTableColumn *tabCol = [[outlineView tableColumns] objectAtIndex:0];
    IBAnnotationTableViewCell *tableViewCell = (IBAnnotationTableViewCell*)[self outlineView:outlineView viewForTableColumn:tabCol item:item];
    float height = [tableViewCell getHeightOfCell];
    return height;
}

- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
    IBAnnotationTableViewCell *tableViewCell = [outlineView makeViewWithIdentifier:@"AnnotationTableViewCell" owner:self];
    PDFAnnotation *annotation = (PDFAnnotation *)item;
    [tableViewCell setupWithPDFAnnotation:annotation];
    return tableViewCell;
}

En mi IBAnnotationTableViewCellcuál es el controlador de mi celda (subclase de NSTableCellView) tengo un método de configuración

-(void)setupWithPDFAnnotation:(PDFAnnotation*)annotation;

que configura todos los puntos de venta y establece el texto de mis PDFAnnotations. Ahora puedo calcular "fácilmente" la altura usando:

-(float)getHeightOfCell
{
    return [self getHeightOfContentTextView] + 60;
}

-(float)getHeightOfContentTextView
{
    NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:[self.contentTextView font],NSFontAttributeName,nil];
    NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:[self.contentTextView string] attributes:attributes];
    CGFloat height = [self heightForWidth: [self.contentTextView frame].size.width forString:attributedString];
    return height;
}

.

- (NSSize)sizeForWidth:(float)width height:(float)height forString:(NSAttributedString*)string
{
    NSInteger gNSStringGeometricsTypesetterBehavior = NSTypesetterLatestBehavior ;
    NSSize answer = NSZeroSize ;
    if ([string length] > 0) {
        // Checking for empty string is necessary since Layout Manager will give the nominal
        // height of one line if length is 0.  Our API specifies 0.0 for an empty string.
        NSSize size = NSMakeSize(width, height) ;
        NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:size] ;
        NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:string] ;
        NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init] ;
        [layoutManager addTextContainer:textContainer] ;
        [textStorage addLayoutManager:layoutManager] ;
        [layoutManager setHyphenationFactor:0.0] ;
        if (gNSStringGeometricsTypesetterBehavior != NSTypesetterLatestBehavior) {
            [layoutManager setTypesetterBehavior:gNSStringGeometricsTypesetterBehavior] ;
        }
        // NSLayoutManager is lazy, so we need the following kludge to force layout:
        [layoutManager glyphRangeForTextContainer:textContainer] ;

        answer = [layoutManager usedRectForTextContainer:textContainer].size ;

        // Adjust if there is extra height for the cursor
        NSSize extraLineSize = [layoutManager extraLineFragmentRect].size ;
        if (extraLineSize.height > 0) {
            answer.height -= extraLineSize.height ;
        }

        // In case we changed it above, set typesetterBehavior back
        // to the default value.
        gNSStringGeometricsTypesetterBehavior = NSTypesetterLatestBehavior ;
    }

    return answer ;
}

.

- (float)heightForWidth:(float)width forString:(NSAttributedString*)string
{
    return [self sizeForWidth:width height:FLT_MAX forString:string].height ;
}
Sunkas
fuente
¿Dónde se implementa heightForWidth: forString:?
ZS
Lo siento por eso. Lo agregó a la respuesta. Intenté encontrar la publicación SO original de la que obtuve esto, pero no pude encontrarla.
Sunkas
1
¡Gracias! Casi funciona para mí, pero me da resultados inexactos. En mi caso, el ancho que estoy introduciendo en el NSTextContainer parece incorrecto. ¿Está utilizando el diseño automático para definir las subvistas de su NSTableViewCell? ¿De dónde proviene el ancho de "self.contentTextView"?
ZS
Publiqué una pregunta sobre mi problema aquí: stackoverflow.com/questions/22929441/…
ZS
3

Estuve buscando una solución durante bastante tiempo y se me ocurrió la siguiente, que funciona muy bien en mi caso:

- (double)tableView:(NSTableView *)tableView heightOfRow:(long)row
{
    if (tableView == self.tableViewTodo)
    {
        CKRecord *record = [self.arrayTodoItemsFiltered objectAtIndex:row];
        NSString *text = record[@"title"];

        double someWidth = self.tableViewTodo.frame.size.width;
        NSFont *font = [NSFont fontWithName:@"Palatino-Roman" size:13.0];
        NSDictionary *attrsDictionary =
        [NSDictionary dictionaryWithObject:font
                                    forKey:NSFontAttributeName];
        NSAttributedString *attrString =
        [[NSAttributedString alloc] initWithString:text
                                        attributes:attrsDictionary];

        NSRect frame = NSMakeRect(0, 0, someWidth, MAXFLOAT);
        NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
        [[tv textStorage] setAttributedString:attrString];
        [tv setHorizontallyResizable:NO];
        [tv sizeToFit];

        double height = tv.frame.size.height + 20;

        return height;
    }

    else
    {
        return 18;
    }
}
vomako
fuente
En mi caso, quería expandir una columna dada verticalmente para que el texto que muestre en esa etiqueta se ajuste todo. Así que obtengo la columna en cuestión y tomo la fuente y el ancho de ella.
Klajd Deda
3

Como uso personalizado NSTableCellViewy tengo acceso al, NSTextFieldmi solución fue agregar un método en NSTextField.

@implementation NSTextField (IDDAppKit)

- (CGFloat)heightForWidth:(CGFloat)width {
    CGSize size = NSMakeSize(width, 0);
    NSFont*  font = self.font;
    NSDictionary*  attributesDictionary = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
    NSRect bounds = [self.stringValue boundingRectWithSize:size options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading attributes:attributesDictionary];

    return bounds.size.height;
}

@end
Klajd Deda
fuente
¡Gracias! ¡Eres mi salvador! Este es el único que funciona correctamente. Pasé un día entero para resolver esto.
Tommy
2

Esto es lo que hice para solucionarlo:

Fuente: busque en la documentación de XCode, en "altura de fila nstableview". Encontrará un código fuente de muestra llamado "TableViewVariableRowHeights / TableViewVariableRowHeightsAppDelegate.m"

(Nota: estoy mirando la columna 1 en la vista de tabla, tendrá que ajustar para buscar en otra parte)

en Delegate.h

IBOutlet NSTableView            *ideaTableView;

en Delegate.m

la vista de tabla delega el control de la altura de la fila

    - (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row {
    // Grab the fully prepared cell with our content filled in. Note that in IB the cell's Layout is set to Wraps.
    NSCell *cell = [ideaTableView preparedCellAtColumn:1 row:row];

    // See how tall it naturally would want to be if given a restricted with, but unbound height
    CGFloat theWidth = [[[ideaTableView tableColumns] objectAtIndex:1] width];
    NSRect constrainedBounds = NSMakeRect(0, 0, theWidth, CGFLOAT_MAX);
    NSSize naturalSize = [cell cellSizeForBounds:constrainedBounds];

    // compute and return row height
    CGFloat result;
    // Make sure we have a minimum height -- use the table's set height as the minimum.
    if (naturalSize.height > [ideaTableView rowHeight]) {
        result = naturalSize.height;
    } else {
        result = [ideaTableView rowHeight];
    }
    return result;
}

también necesita esto para afectar la nueva altura de fila (método delegado)

- (void)controlTextDidEndEditing:(NSNotification *)aNotification
{
    [ideaTableView reloadData];
}

Espero que esto ayude.

Nota final: esto no admite el cambio de ancho de columna.

Laurent
fuente
¡Tú Molas! :) Gracias.
abanderado
4
Desafortunadamente, el código de muestra de Apple (que actualmente está marcado como versión 1.1) usa una tabla basada en celdas, no una tabla basada en vistas (de lo que trata esta pregunta). Las llamadas de código -[NSTableView preparedCellAtColumn:row], que los documentos dicen "solo están disponibles para las vistas de tabla basadas en NSCell". El uso de este método en una tabla basada en vistas produce esta salida de registro en tiempo de ejecución: "Error de vista NSTableView basada en vista: se llamó a la columna preparadaCellAt: fila:. Registre un error con el seguimiento de este registro o deje de usar el método".
Ashley
1

Aquí hay una solución basada en la respuesta de JanApotheker, modificada porque cellView.fittingSize.heightno me devolvía la altura correcta. En mi caso, estoy usando el estándar NSTableCellView, un NSAttributedStringpara el texto del campo de texto de la celda y una tabla de una sola columna con restricciones para el campo de texto de la celda establecido en IB.

En mi controlador de vista, declaro:

var tableViewCellForSizing: NSTableCellView?

En viewDidLoad ():

tableViewCellForSizing = tableView.make(withIdentifier: "My Identifier", owner: self) as? NSTableCellView

Finalmente, para el método delegado tableView:

func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
    guard let tableCellView = tableViewCellForSizing else { return minimumCellHeight }

    tableCellView.textField?.attributedStringValue = attributedString[row]
    if let height = tableCellView.textField?.fittingSize.height, height > 0 {
        return height
    }

    return minimumCellHeight
}

mimimumCellHeightes una constante establecida en 30, para respaldo, pero nunca se usa realmente. attributedStringses mi matriz modelo de NSAttributedString.

Esto funciona perfectamente para mis necesidades. Gracias por todas las respuestas anteriores, que me indicaron la dirección correcta para este molesto problema.

jbaraga
fuente
1
Muy bien. Un problema. La tabla no puede ser editable o textfield.fittingSize.height no funcionará y devolverá cero. Establecer table.isEditable = false en viewdidload
jiminybob99
0

Esto suena mucho a algo que tenía que hacer anteriormente. Ojalá pudiera decirles que se me ocurrió una solución simple y elegante, pero, por desgracia, no lo hice. Sin embargo, no por falta de intentos. Como ya ha notado, la necesidad de UITableView de conocer la altura antes de que se construyan las celdas realmente hace que todo parezca bastante circular.

Mi mejor solución fue llevar la lógica a la celda, porque al menos podía aislar qué clase necesitaba para comprender cómo se distribuían las celdas. Un método como

+ (CGFloat) heightForStory:(Story*) story

podría determinar la altura que tenía que tener la celda. Por supuesto, eso implicaba medir texto, etc. En algunos casos, ideé formas de almacenar en caché la información obtenida durante este método que luego podría usarse cuando se creó la celda. Eso fue lo mejor que se me ocurrió. Es un problema exasperante, aunque parece que debería haber una mejor respuesta.

vagabundo
fuente