objective c - NSTableView basado en la vista con filas que tienen alturas dinámicas
objective-c macos (10)
¿Has RowResizableViews un vistazo a RowResizableViews ? Es bastante viejo y no lo he probado pero puede funcionar.
Tengo una aplicación con un NSTableView
basado en vista. Dentro de esta vista de tabla, tengo filas que tienen celdas que tienen contenido que consiste en un NSTextField
varias filas con habilitación de palabras habilitada. 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 NSTextField
palabras utilizado en NSTextField
. La palabra envoltura de NSTextField
se basa de manera similar en cuán ancho es el NSTextField
... que está determinado por el ancho de NSTableView
.
Soooo ... 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 desastre complicado. Dado que TableView requiere conocimiento de la altura de las celdas para establecerlas ... y NSTextField
necesita conocimiento de su diseño para determinar el NSTextField
de palabras ... y la celda necesita conocimiento de la envoltura de palabras para determinar su altura ... es un desastre circular ... y me está volviendo loco .
Sugerencias?
Si es importante, el resultado final también tendrá NSTextFields
editables que se redimensionarán para ajustarse al texto que NSTextFields
. Ya tengo este trabajo 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 obtenga el problema de la altura, noteHeightOfRowsWithIndexesChanged
método - noteHeightOfRowsWithIndexesChanged
para informar a la vista de la tabla de la altura modificada ... pero aún así va a pedir al delegado la altura ... por lo tanto, mi quandry.
¡Gracias por adelantado!
Aquí hay una solución basada en la respuesta de JanApotheker, modificada como cellView.fittingSize.height
no me devolvió la altura correcta. En mi caso, estoy usando el NSTableCellView
estándar, un NSAttributedString
para el texto NSAttributedString
de la celda y una tabla de columna única con restricciones para el campo 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 copia de seguridad, pero nunca se usa realmente. attributedStrings
es mi matriz modelo de NSAttributedString
.
Esto funciona perfectamente para mis necesidades. Gracias por todas las respuestas anteriores, que me orientaron en la dirección correcta para este molesto problema.
Basado en la respuesta de Corbin (gracias por cierto, arrojando algo de luz sobre esto):
Swift 3, NSTableView basado en vista con diseño automático para macOS 10.11 (y superior)
Mi configuración: tengo una NSTableCellView
que se distribuye usando Diseño automático. Contiene (además de otros elementos) un NSTextField
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 decir a la vista de tabla para actualizar 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 solicite su delegado para 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 de modelo y le pedimos que vuelva a diseñar todo en función del ancho de la vista de tabla actual. La altura del tamaño de fittingSize
se puede usar como la nueva altura.
Como utilizo NSTableCellView
personalizado y tengo acceso a 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
Estaba buscando una solución por 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;
}
}
Este es un problema de huevo y pollo. La tabla necesita saber la altura de la fila porque eso determina dónde se ubicará una vista determinada. Pero desea que la vista ya esté disponible para que pueda usarla para calcular la altura de la fila. Entonces, ¿qué viene primero?
La respuesta es mantener un NSTableCellView
adicional (o la vista que esté usando como su "vista de celda") solo para medir el alto de la vista. En tableView:heightOfRow:
delegar método, acceda a su modelo para ''fila'' y configure objectValue
en NSTableCellView
. Luego, configure el ancho de la vista para que sea el ancho de su tabla, y (como quiera hacerlo) determine la altura requerida para esa vista. Devuelve ese valor.
No noteHeightOfRowsWithIndexesChanged:
from en el método de delegado tableView:heightOfRow:
or viewForTableColumn:row:
Eso es malo y causará mega 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 altura de NSTableCellView
(o la vista que esté utilizando como su "vista de celda"). La tabla debe controlar el marco de esa vista, y lucharás contra la vista de tabla si tratas de configurarla. En cambio, en su destino / acción para el campo de texto donde calculó la altura, llame a noteHeightOfRowsWithIndexesChanged:
que permitirá que la tabla noteHeightOfRowsWithIndexesChanged:
el tamaño de esa fila individual. Suponiendo que tiene su configuración de máscara de aumento automático en las subvistas (es decir, las subvistas de NSTableCellView
), ¡las cosas deberían redimensionarse bien! Si no, 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 olvide que 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 desarrolladores de Apple que al desbordamiento de la pila.
PSS: escribí la vista basada en NSTableView
Esto es lo que hice para solucionarlo:
Fuente: consulte la documentación de XCode, en "altura de la 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 modificar para buscar en otro lado)
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 efectuar la nueva altura de fila (método delegado)
- (void)controlTextDidEndEditing:(NSNotification *)aNotification
{
[ideaTableView reloadData];
}
Espero que esto ayude.
Nota final: esto no permite cambiar el ancho de la columna.
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, solo tiene que seleccionar su NSTableView
o NSOutlineView
en el editor de guiones gráficos y seleccionar esta opción en el Inspector de tamaños:
Luego configura las cosas en su NSTableCellView
para tener restricciones superiores e inferiores a 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
). Puedes ver qué alturas se están calculando para tus filas de diseño automático con este método:
func outlineView(_ outlineView: NSOutlineView, didAdd rowView: NSTableRowView, forRow row: Int) {
print(rowView.fittingSize.height)
}
Esto se parece mucho a algo que tenía que hacer previamente. Ojalá pudiera decirte que se me ocurrió una solución simple y elegante pero, ay, no lo hice. No por falta de intentarlo. Como ya has 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 llevar la lógica a la célula, porque al menos podía aislar qué clase era necesaria para comprender cómo se diseñaban las células. Un método como
+ (CGFloat) heightForStory:(Story*) story
Sería capaz de determinar qué tan alta debía ser la celda. Por supuesto, eso implicó la medición de texto, etc. En algunos casos ideé formas de almacenar en caché la información obtenida durante este método que luego podría utilizarse cuando se creó la célula. Eso fue lo mejor que se me ocurrió. Aunque es un problema exasperante, ya que parece que debería haber una mejor respuesta.
Para cualquiera que quiera más código, aquí está la solución completa que utilicé. Gracias a Corbin Dunn por señalarme en la dirección correcta.
Necesitaba establecer la altura principalmente en relación con cuán 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 de mi celda (subclase de NSTableCellView
) tengo un método de configuración
-(void)setupWithPDFAnnotation:(PDFAnnotation*)annotation;
que establece todos los puntos de venta y establece el texto de mi PDFAnotaciones. Ahora puedo calcutar "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 ;
}