Vista NSTableView basada en vistas con filas que tienen alturas dinámicas


Tengo una aplicación con una vista basada en NSTableView en ella. Dentro de esta vista de tabla, tengo filas que tienen celdas que tienen contenido que consiste en una fila múltiple NSTextField con ajuste de palabra habilitado. Dependiendo del contenido textual de NSTextField, el tamaño de las filas necesarias para mostrar la celda variará.

Sé que puedo implementar el método NSTableViewDelegate- tableView:heightOfRow: para devolver la altura, pero la altura se determinará en función del ajuste de palabras utilizado en el NSTextField. El envoltorio de la NSTextField se basa de manera similar en cuán ancho es el NSTextField which que está determinado por el ancho del NSTableView.

Soooo guess supongo que mi pregunta es what ¿qué es un buen patrón de diseño para esto? Parece que todo lo que intento termina siendo un enrevesado desastre. Dado que la vista de tabla requiere el conocimiento de la altura de las celdas para colocarlas... y el NSTextField necesita conocimiento de su diseño para determinar el ajuste de línea the y la celda necesita conocimiento del ajuste de línea para determinar su altura it es un lío circular and y me está volviendo loco.

Sugerencias?

Si es importante, el resultado final también tendrá NSTextFields editable que se redimensionará para ajustarse al texto dentro de ellos. Ya tengo esto trabajando en el nivel de vista, pero la vista de tabla todavía no ajusta las alturas de las celdas. Me imagino que una vez que tenga el problema de altura resuelto, usaré el método - noteHeightOfRowsWithIndexesChanged para informar a la vista de tabla de la altura cambiada but pero todavía va a preguntar al delegado por la altura hence por lo tanto, mi quandry.

Gracias de antemano!

Author: Andriy, 2011-09-21

10 answers

Este es un problema de pollo y huevo. La tabla necesita conocer la altura de la fila porque eso determina dónde se encuentra una vista dada. Pero desea que una vista ya esté disponible para que pueda usarla para averiguar la altura de la fila. Entonces, ¿qué viene primero?

La respuesta es mantener un NSTableCellView extra (o cualquier vista que esté utilizando como su "vista de celda") alrededor solo para medir la altura de la vista. En el método delegado tableView:heightOfRow:, acceda a su modelo para 'row' y establezca objectValue en NSTableCellView. A continuación, establecer el ancho de la vista debe ser el ancho de la tabla y (como quiera hacerlo) calcular la altura requerida para esa vista. Devuelve ese valor.

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

Para actualizar dinámicamente la altura, entonces lo que debe hacer es responder al cambio de texto (a través del objetivo/acción) y recalcular la altura calculada de esa vista. Ahora, no cambie dinámicamente la altura de NSTableCellView (o lo que sea vista que está utilizando como su"vista de celda"). La tabla debe controlar el marco de esa vista, y luchará contra la vista de tabla si intenta configurarla. En su lugar, en su objetivo/acción para el campo de texto donde calculó la altura, llame a noteHeightOfRowsWithIndexesChanged:, lo que permitirá que la tabla cambie el tamaño de esa fila individual. Suponiendo que usted tiene su configuración de máscara de autoresizing derecha en subviews (es decir: subviews de la NSTableCellView), las cosas deben cambiar de tamaño bien! Si no, primero trabaje en la máscara de cambio de tamaño de las subviews para obtener cosas derecha con alturas de fila variables.

No olvides que noteHeightOfRowsWithIndexesChanged: se anima por defecto. Para que no se anime:

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

PD: Respondo más a las preguntas publicadas en los Foros de Apple Dev que a stack overflow.

PSS: Escribí la vista basada en NSTableView

 125
Author: corbin dunn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-09 14:23:41

Esto 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 solo tienes que seleccionar tu NSTableView o NSOutlineView en el editor de storyboard y seleccionar esta opción en el Inspector de tamaño:

introduzca la descripción de la imagen aquí

Luego configura las cosas en su NSTableCellView para que tengan restricciones superiores e inferiores a la la celda y su celda cambiarán de tamaño para que quepa automáticamente. No se requiere código!

Su aplicación ignorará cualquier altura especificada 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)
}
 14
Author: Clifton Labrum,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-01 23:27:09

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

Necesitaba establecer la altura principalmente en relación con qué tan alto era un NSTextView en mi NSTableViewCell.

En mi subclase de NSViewController Creo temporalmente una nueva celda llamando a outlineView: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 IBAnnotationTableViewCell que es el controlador para mi celda (subclase de NSTableCellView) Tengo un método de configuración

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

Que establece todas las salidas y establece el texto de mi PDFAnnotations. Ahora puedo "fácilmente" calcular 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 ;
}
 7
Author: Sunkas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-04-07 18:58:08

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

Swift 3, Vista NSTableView basada en vistas con Diseño automático para macOS 10.11 (y superior)

Mi configuración: Tengo un NSTableCellView que se presenta usando Auto-Layout. Contiene (además de otros elementos) un NSTextField multilínea que 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 decirle 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 el objeto del modelo de datos cambia:

tableView.noteHeightOfRows(withIndexesChanged: changedIndexes)

Esto hará que la vista de tabla pida a su delegado la nueva altura de la 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 comprobamos si ya hemos creado una vista para este identificador. Para ello 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 ninguno es salvo, nosotros necesidad de crear (y caché) una nueva. Actualizamos la vista de celda con nuestro objeto modelo y le decimos que reorganice todo en función del ancho de vista de la tabla actual. La altura fittingSize se puede usar como la nueva altura.

 7
Author: JanApotheker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-17 09:49:08

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;
    }
}
 3
Author: vomako,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-25 09:41:20

Dado que uso personalizado NSTableCellView y tengo acceso a la NSTextField mi 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
 3
Author: Klajd Deda,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-01 13:42:25

¿Has visto RowResizableViews? Es bastante viejo y no lo he probado, pero puede funcionar.

 2
Author: Tim,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-09-21 21:13:26

Esto es lo que he hecho para arreglarlo:

Fuente: Busque en la documentación de XCode, en "row height nstableview". Encontrará un código fuente de ejemplo llamado " TableViewVariableRowHeights / TableViewVariableRowHeightsAppDelegate.m "

(Nota: Estoy mirando la columna 1 en la vista de tabla, tendrá que ajustar para buscar en otro lugar)

En Delegado.h

IBOutlet NSTableView            *ideaTableView;

En Delegado.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;
}

Usted también necesita esto para efectuar 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.

 2
Author: laurent,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-09-17 09:32:56

Esto se parece mucho a algo que tenía que hacer anteriormente. Desearía poder decirles que se me ocurrió una solución simple y elegante, pero, por desgracia, no lo hice. No por falta de intentarlo. Como ya ha notado, la necesidad de UITableView para saber la altura antes de que las celdas se construyan realmente hace que todo parezca bastante circular.

Mi mejor solución fue empujar la lógica a la célula, porque al menos podía aislar qué clase necesitaba para entender cómo se diseñaban las células. Método como

+ (CGFloat) heightForStory:(Story*) story

Sería capaz de determinar la altura de la célula tenía que ser. Por supuesto que 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ó. Sin embargo, es un problema exasperante, ya que parece que debería haber una mejor respuesta.

 1
Author: vagrant,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-09-30 05:06:37

Aquí hay una solución basada en la respuesta de JanApotheker, modificada como cellView.fittingSize.height no estaba devolviendo la altura correcta para mí. En mi caso estoy usando el estándar NSTableCellView, un NSAttributedString para el texto TextField de la celda, y una tabla de una sola columna con restricciones para el TextField 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
}

mimimumCellHeight es un conjunto constante a 30, para la copia de seguridad, pero nunca se usó. attributedStrings es mi matriz modelo de NSAttributedString.

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

 1
Author: jbaraga,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-29 16:56:14