NSTableView baseado em visualização com linhas que possuem alturas dinâmicas

Eu tenho um aplicativo com um NSTableView baseado em NSTableView nele. Dentro dessa visualização de tabela, eu tenho linhas que possuem células que têm conteúdo composto por um NSTextField várias linhas com quebra automática de linha ativada. Dependendo do conteúdo textual do NSTextField , o tamanho das linhas necessárias para exibir a célula irá variar.

Eu sei que posso implementar o método NSTableViewDelegatetableView:heightOfRow: para retornar a altura, mas a altura será determinada com base na quebra tableView:heightOfRow: palavras usada no NSTextField . A quebra de linha do NSTextField é similarmente baseada na largura do NSTextField que é determinado pela largura do NSTableView .

Soooo… Eu acho que a minha pergunta é… o que é um bom padrão de design para isso? Parece que tudo que eu tento acaba sendo uma bagunça complicada. Como o TableView requer conhecimento da altura das células para colocá-las para fora … e o NSTextField precisa de conhecimento do layout para determinar a quebra de linha … e a célula precisa de conhecimento da quebra de linha para determinar sua altura … é uma bagunça circular … E isso está me deixando louco .

Sugestões?

Se for importante, o resultado final também terá NSTextFields editáveis ​​que serão redimensionados para se ajustarem ao texto dentro deles. Eu já tenho isso trabalhando no nível de exibição, mas a tableview ainda não ajusta as alturas das células. Eu acho que uma vez que eu resolvo o problema da altura, usarei o método – noteHeightOfRowsWithIndexesChanged para informar a visão da tabela sobre a altura alterada … mas ainda vai perguntar ao delegado a altura … daí, meu quandry.

Desde já, obrigado!

    Este é um problema de galinha e ovo. A tabela precisa saber a altura da linha porque isso determina onde uma determinada vista ficará. Mas você quer que uma visão já esteja por perto, para que você possa usá-la para descobrir a altura da linha. Então, o que vem primeiro?

    A resposta é manter um NSTableCellView extra (ou qualquer que seja a visão que você esteja usando como sua “visão de célula”) apenas para medir a altura da visão. No método tableView:heightOfRow: delegate, acesse seu modelo para ‘row’ e defina o objectValue em NSTableCellView . Em seguida, defina a largura da vista como a largura da sua mesa e (no entanto, você quer fazê-lo) descobrir a altura necessária para essa visão. Devolva esse valor.

    Não chame noteHeightOfRowsWithIndexesChanged: no método delegate tableView:heightOfRow: ou viewForTableColumn:row: Isso é ruim e causará mega-problemas.

    Para atualizar dinamicamente a altura, o que você deve fazer é responder à alteração do texto (por meio do alvo / ação) e recalcular a altura calculada dessa visualização. Agora, não mude dinamicamente a NSTableCellView do NSTableCellView (ou qualquer que seja a vista que você esteja usando como sua “visão de célula”). A tabela deve controlar o quadro dessa visão e você estará lutando com a tableview se tentar configurá-la. Em vez disso, em seu destino / ação para o campo de texto em que você calculou a altura, chame noteHeightOfRowsWithIndexesChanged: que permitirá que a tabela redimensione essa linha individual. Supondo que você tenha sua configuração de máscara de NSTableCellViewNSTableCellView nas NSTableCellView (isto é, NSTableCellView do NSTableCellView ), as coisas devem ser redimensionadas bem! Se não, primeiro trabalhe na máscara de redimensionamento das subvisualizações para acertar as coisas com alturas de linha variables.

    Não esqueça que noteHeightOfRowsWithIndexesChanged: anima por padrão. Para não animar:

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

    PS: Eu respondo mais às perguntas postadas nos Fóruns de Desenvolvimento da Apple do que ao estouro de pilha.

    PSS: eu escrevi o modo de exibição baseado em NSTableView

    Isso ficou muito mais fácil no macOS 10.13 com .usesAutomaticRowHeights . Os detalhes estão aqui: https://developer.apple.com/library/content/releasenotes/AppKit/RN-AppKit/#10_13 (Na seção intitulada “NSTableView Automatic Row Heights”).

    Basicamente, basta selecionar o seu NSTableView ou NSOutlineView no editor de storyboard e selecionar essa opção no Inspetor de tamanho:

    insira a descrição da imagem aqui

    Em seguida, você configura as coisas em sua NSTableCellView para ter as restrições de cima e de baixo na célula e sua célula será redimensionada para caber automaticamente. Nenhum código requerido!

    Seu aplicativo ignorará qualquer altura especificada em heightOfRow ( NSTableView ) e heightOfRowByItem ( NSOutlineView ). Você pode ver quais alturas estão sendo calculadas para suas linhas de layout automático com este método:

     func outlineView(_ outlineView: NSOutlineView, didAdd rowView: NSTableRowView, forRow row: Int) { print(rowView.fittingSize.height) } 

    Para quem quiser mais código, aqui está a solução completa que usei. Obrigado corbin dunn por me apontar na direção certa.

    Eu precisava definir a altura principalmente em relação a quão alto era um NSTextView na minha NSTableViewCell .

    Na minha subclass de NSViewController eu temporariamente criar uma nova célula chamando 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; } 

    No meu IBAnnotationTableViewCell que é o controlador da minha célula (subclass de NSTableCellView ) eu tenho um método de configuração

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

    que configura todas as tomadas e define o texto do meu PDFAnnotations. Agora eu posso “facilmente” calcutar a 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 ; } 

    Com base na resposta de Corbin (btw graças derramando alguma luz sobre isso):

    Swift 3, NSTableView baseado em visualização com layout automático para macOS 10.11 (e acima)

    Minha configuração: Eu tenho um NSTableCellView que é apresentado usando o Layout automático. Ele contém (além de outros elementos) um NSTextField várias linhas que pode ter até 2 linhas. Portanto, a altura da exibição de célula inteira depende da altura desse campo de texto.

    Eu atualizo digo a visão de tabela para atualizar a altura em duas ocasiões:

    1) Quando a vista de tabela é redimensionada:

     func tableViewColumnDidResize(_ notification: Notification) { let allIndexes = IndexSet(integersIn: 0.. 

    2) Quando o object do modelo de dados é alterado:

     tableView.noteHeightOfRows(withIndexesChanged: changedIndexes) 

    Isso fará com que a exibição da tabela peça a ele delegação para a nova altura da linha.

     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 } 

    Primeiro, precisamos obter nosso object de modelo para a linha ( entity ) e o identificador de visualização de célula apropriado. Em seguida, verificamos se já criamos uma visualização para esse identificador. Para fazer isso, precisamos manter uma lista com visualizações de células para cada identificador:

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

    Se nenhum for salvo, precisamos criar (e armazenar em cache) um novo. Atualizamos a visualização da célula com o nosso object de modelo e informamos para re-layout de tudo com base na largura da visualização da tabela atual. A altura fittingSize pode então ser usada como a nova altura.

    Eu estava procurando uma solução há algum tempo e surgiu a seguinte, que funciona muito bem no meu 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; } } 

    Desde que eu uso NSTableCellView personalizado e tenho access ao NSTextField minha solução foi adicionar um método em 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 

    Você já deu uma olhada no RowResizableViews ? É bastante antigo e eu não testei, mas ainda assim pode funcionar.

    Aqui está o que eu fiz para consertar isso:

    Fonte: Consulte a documentação do XCode, em “altura da linha nstableview”. Você encontrará um código-fonte de amostra chamado “TableViewVariableRowHeights / TableViewVariableRowHeightsAppDelegate.m”

    (Nota: eu estou olhando para a coluna 1 no modo de exibição de tabela, você terá que ajustar para procurar em outro lugar)

    em Delegate.h

     IBOutlet NSTableView *ideaTableView; 

    em Delegate.m

    vista de tabela delega o controle da altura da linha

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

    você também precisa disso para efetuar a nova altura da linha (método delegado)

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

    Eu espero que isso ajude.

    Nota final: isso não suporta alterar a largura da coluna.

    Isso parece muito com algo que eu tive que fazer anteriormente. Eu gostaria de poder lhe dizer que encontrei uma solução simples e elegante, mas, infelizmente, não consegui. Não por falta de tentativa embora. Como você já percebeu a necessidade do UITableView de saber a altura antes de as células serem construídas realmente fazer tudo parecer bem circular.

    Minha melhor solução foi empurrar a lógica para a célula, porque pelo menos eu poderia isolar a class necessária para entender como as células eram dispostas. Um método como

     + (CGFloat) heightForStory:(Story*) story 

    seria capaz de determinar o quão alto a célula tinha que ser. É claro que isso envolveu a medição de texto, etc. Em alguns casos, criei maneiras de armazenar em cache as informações obtidas durante esse método que poderiam ser usadas quando a célula fosse criada. Isso foi o melhor que eu inventei. É um problema irritante, embora pareça que deveria haver uma resposta melhor.

    Aqui está uma solução baseada na resposta do JanApotheker, modificada como cellView.fittingSize.height não estava retornando a altura correta para mim. No meu caso, estou usando o NSTableCellView padrão, um NSAttributedString para o texto textField da célula e uma tabela de coluna única com restrições para o textField da célula definida no IB.

    No meu controlador de visualização, declaro:

     var tableViewCellForSizing: NSTableCellView? 

    Em viewDidLoad ():

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

    Finalmente, para o 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 é uma constante configurada para 30, para backup, mas nunca usada de fato. NSAttributedString é o array do meu modelo de NSAttributedString .

    Isso funciona perfeitamente para minhas necessidades. Obrigado por todas as respostas anteriores, que me apontaram na direção certa para este problema traquina.