NSTableView на основе представлений со строками с динамической высотой

84

У меня есть приложение с view-based NSTableViewв нем. Внутри этого табличного представления у меня есть строки с ячейками, содержимое которых состоит из нескольких строк NSTextFieldс включенным переносом слов. В зависимости от текстового содержимого NSTextField, размер строк, необходимых для отображения ячейки, будет варьироваться.

Я знаю, что могу реализовать NSTableViewDelegateметод - tableView:heightOfRow:вернуть высоту, но высота будет определяться на основе переноса слов, используемого в NSTextField. Перенос слов NSTextFieldаналогичным образом основан на ширине NSTextField..., которая определяется шириной NSTableView.

Тааааааааааааааааааааааааалов или)))) вот в чем заключается мой вопрос. Какой шаблон проектирования для этого подходит? Кажется, все, что я пробую, превращается в запутанный беспорядок. Поскольку TableView требует знания высоты ячеек для их расположения ... и NSTextFieldзнания его макета для определения переноса слов… а ячейке требуется знание переноса слов, чтобы определить его высоту… это круговой беспорядок… и это сводит меня с ума .

Предложения?

Если это имеет значение, конечный результат также будет доступен для редактирования NSTextFields, размер которого будет изменяться в соответствии с текстом внутри них. У меня уже есть эта работа на уровне представления, но представление таблицы еще не регулирует высоту ячеек. Я полагаю, что как только проблема с высотой будет решена, я воспользуюсь noteHeightOfRowsWithIndexesChangedметодом -, чтобы сообщить представлению таблицы, что высота изменилась ... но он все равно будет запрашивать у делегата высоту ... отсюда и мои затруднения.

Заранее спасибо!

Джива Дево
источник
Если и ширина, и высота представления могут изменяться вместе с их содержимым, то их определение сложно, потому что вам нужно выполнять итерацию (но тогда вы можете кэшировать). Но в вашем случае высота меняется при постоянной ширине. Если ширина известна (т.е. основана на ширине окна или представления), то здесь нет ничего сложного. Мне не хватает, почему ширина меняется?
Роб Напье
Только высота может изменяться вместе с содержимым. И я уже решил эту проблему. У меня есть подкласс NSTextField, который автоматически регулирует его высоту. Проблема заключается в том, чтобы получить сведения об этой настройке обратно делегату табличного представления, чтобы он мог соответствующим образом обновить высоту.
Jiva DeVoe
@Jiva: Интересно, решил ли ты эту проблему?
Джошуа Ноцци
Я пытаюсь сделать что-то подобное. Мне нравится идея подкласса NSTextField, который регулирует свою высоту. Как насчет добавления уведомления (делегата) об изменении высоты в этот подкласс и отслеживания этого с помощью некоторого соответствующего класса (источник данных, делегат структуры, ...), чтобы получить информацию в схеме?
Джон Велман
Связанный вопрос с ответами, которые помогли мне больше, чем здесь: Высота строки NSTableView на основе NSStrings .
Эшли

Ответы:

133

Это проблема курицы и яйца. Таблица должна знать высоту строки, потому что она определяет, где будет располагаться данное представление. Но вы хотите, чтобы представление уже было, чтобы вы могли использовать его для определения высоты строки. Итак, что на первом месте?

Ответ заключается в том, чтобы оставить дополнительный NSTableCellView(или любой другой вид, который вы используете в качестве «представления ячеек») только для измерения высоты представления. В tableView:heightOfRow:методе делегата, доступ к вашей модели для «строки» и установите objectValueна NSTableCellView. Затем установите ширину представления равной ширине вашей таблицы и (как бы вы это ни хотели) вычислите требуемую высоту для этого представления. Верните это значение.

Не вызывайте noteHeightOfRowsWithIndexesChanged:from в методе делегата tableView:heightOfRow:или viewForTableColumn:row:! Это плохо и вызовет мега-проблемы.

Чтобы динамически обновлять высоту, вам следует отреагировать на изменение текста (через цель / действие) и пересчитать вычисленную высоту этого представления. Теперь не изменяйте динамически NSTableCellViewвысоту (или любое другое представление, которое вы используете в качестве «представления ячейки»). Таблица должна управлять фреймом этого представления, и вы будете бороться с табличным представлением, если попытаетесь его установить. Вместо этого в вашей цели / действии для текстового поля, где вы вычислили высоту, вызовите noteHeightOfRowsWithIndexesChanged:, что позволит таблице изменить размер этой отдельной строки. Предполагая, что у вас есть настройка маски с автоматическим изменением размера прямо на подпредставлениях (то есть: на подвидах NSTableCellView), все должно измениться в размере! Если нет, сначала поработайте над маской изменения размера вложенных представлений, чтобы все было правильно с переменной высотой строк.

Не забывайте, что noteHeightOfRowsWithIndexesChanged:по умолчанию анимация. Чтобы не было анимации:

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

PS: Я отвечаю больше на вопросы, размещенные на форумах Apple Dev, чем на переполнение стека.

PSS: я написал NSTableView на основе представления

Корбин Данн
источник
Спасибо за ответ. Я не видел его изначально ... Я отметил его как правильный ответ. Форумы разработчиков отличные, я ими тоже пользуюсь.
Jiva DeVoe
«определить требуемую высоту для этого представления. Вернуть это значение». .. Это моя проблема :(. Я использую tableView на основе представлений, и я понятия не имею, как это сделать!
Mazyod
@corbin Спасибо за ответ, это то, что у меня было в моем приложении. Однако я обнаружил проблему с Mavericks. Поскольку вы хорошо разбираетесь в этом предмете, не могли бы вы проверить мой вопрос? Здесь: stackoverflow.com/questions/20924141/…
Alex
1
@corbin есть ли на это канонический ответ, но с использованием автоматической компоновки и ограничений с NSTableViews? Возможное решение - автоматическая верстка; интересно , если это достаточно хорошо без estimatedHeightForRowAtIndexPath интерфейсов API (что разве в какао)
ZS
3
@ZS: Я использую Auto Layout и реализовал ответ Корбина. В tableView:heightOfRow:, я настроил свое представление резервной ячейки со значениями строки (у меня только один столбец) и вернулся cellView.fittingSize.height. fittingSize- это метод NSView, который вычисляет минимальный размер представления, удовлетворяющий его ограничениям.
Питер Хози
43

Это стало намного проще в macOS 10.13 с расширением.usesAutomaticRowHeights . Подробности здесь: https://developer.apple.com/library/content/releasenotes/AppKit/RN-AppKit/#10_13 (в разделе под названием «NSTableView Automatic Row Heights»).

В основном вы просто выбираете NSTableViewили NSOutlineViewв редакторе раскадровки и выбираете эту опцию в инспекторе размеров:

введите описание изображения здесь

Затем вы устанавливаете в своем материале NSTableCellViewверхние и нижние ограничения для ячейки, и ваша ячейка автоматически изменится по размеру. Код не требуется!

Ваше приложение будет игнорировать любые высоты, указанные в heightOfRow( NSTableView) и heightOfRowByItem( NSOutlineView). Вы можете увидеть, какие высоты рассчитываются для строк вашего автоматического макета, с помощью этого метода:

func outlineView(_ outlineView: NSOutlineView, didAdd rowView: NSTableRowView, forRow row: Int) {
  print(rowView.fittingSize.height)
}
Клифтон Лабрам
источник
@tofutim: если работает правильно с обернутыми текстовыми ячейками, если вы используете правильные ограничения и когда вы убедитесь, что для параметра Preferred Width каждого NSTextField с динамической высотой установлено значение Automatic. См. Также stackoverflow.com/questions/46890144/…
Ely
Я считаю, что @tofutim прав, это не сработает, если вы поместите NSTextView в TableView, он попытается заархивировать его, но это не сработает. Однако это работает для других компонентов, например NSImage ..
Альберт
Я использовал фиксированную высоту для ряда, но у меня это не работает
Кен Зира
2
После нескольких дней выяснения, почему это не работает для меня, я обнаружил: он работает ТОЛЬКО, когда его нельзя TableCellViewредактировать. Не имеет значения, проверено в Атрибутах [Поведение: редактируется] или Инспекторе привязок [Условно установлено редактируемое: Да]
Лукаш
Я просто хочу подчеркнуть, насколько важно иметь фактические ограничения по высоте в ячейке таблицы: у меня этого не было, поэтому расчетный размер fitSize ячейки был равен 0,0, и это вызвало разочарование.
green_knight
9

Основываясь на ответе Корбина (кстати, спасибо, что пролило свет на это):

Swift 3, NSTableView на основе представления с автоматической компоновкой для macOS 10.11 (и выше)

Моя настройка: у меня есть файл, NSTableCellViewкоторый разложен с помощью Auto-Layout. Он содержит (помимо других элементов) многострочную строку, NSTextFieldкоторая может иметь до 2 строк. Следовательно, высота всего представления ячейки зависит от высоты этого текстового поля.

Я обновляю, чтобы вид таблицы обновлял высоту в двух случаях:

1) При изменении размера представления таблицы:

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

2) При изменении объекта модели данных:

tableView.noteHeightOfRows(withIndexesChanged: changedIndexes)

Это заставит представление таблицы запросить у делегата новую высоту строки.

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
}

Во-первых, нам нужно получить объект нашей модели для row ( entity) и соответствующий идентификатор представления ячейки. Затем мы проверяем, создали ли мы уже представление для этого идентификатора. Для этого мы должны вести список с представлениями ячеек для каждого идентификатора:

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

Если ничего не сохранено, нам нужно создать (и кэшировать) новый. Мы обновляем представление ячейки с помощью нашего объекта модели и приказываем ему изменить макет всего на основе текущей ширины представления таблицы. Затем fittingSizeвысоту можно использовать как новую высоту.

Ян Апотекер
источник
Это идеальный ответ. Если у вас несколько столбцов, вам нужно установить границы по ширине столбца, а не по ширине таблицы.
Войто
Кроме того, после установки cellView.bounds.size.width = tableView.bounds.size.widthрекомендуется сбросить высоту на меньшее значение. Если вы планируете повторно использовать этот фиктивный вид, установка меньшего числа приведет к "сбросу" подходящего размера.
Войто
7

Для тех, кто хочет больше кода, вот полное решение, которое я использовал. Спасибо, Корбин Данн, за то, что указал мне в правильном направлении.

Мне нужно было установить высоту в основном в зависимости от того, насколько высока NSTextViewу меня NSTableViewCell.

В моем подклассе NSViewControllerя временно создаю новую ячейку, вызывая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;
}

В моем IBAnnotationTableViewCellконтроллере для моей ячейки (подкласса NSTableCellView) у меня есть метод настройки

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

который настраивает все выходы и устанавливает текст из моих PDFAnnotations. Теперь я могу «легко» вычислить высоту, используя:

-(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 ;
}
Сункас
источник
Где реализована heightForWidth: forString:?
ZS
Прости за это. Добавил в ответ. Пытался найти исходное сообщение SO, из которого я получил это, но не смог его найти.
Sunkas 07
1
Благодаря! У меня это почти работает, но дает неточные результаты. В моем случае ширина, которую я загружаю в NSTextContainer, кажется неправильной. Используете ли вы автоматическую компоновку для определения подпредставлений вашего NSTableViewCell? Откуда берется ширина "self.contentTextView"?
ZS
Я разместил вопрос о своей проблеме здесь: stackoverflow.com/questions/22929441/…
ZS
3

Я довольно долго искал решение и пришел к следующему, который отлично работает в моем случае:

- (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;
    }
}
вомако
источник
В моем случае я хотел расширить заданный столбец по вертикали, чтобы текст, который я отображаю на этой этикетке, уместился полностью. Поэтому я получаю столбец, о котором идет речь, и беру из него шрифт и ширину.
Klajd Deda
3

Поскольку я использую обычай, NSTableCellViewи у меня есть доступ к NSTextFieldмоему решению, я добавил метод 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
Клайд Деда
источник
Благодаря! Ты мой спаситель! Это единственное, что делает правильно. Я потратил целый день, чтобы понять это.
Tommy
2

Вот что я сделал, чтобы это исправить:

Источник: посмотрите документацию XCode в разделе «Высота строки nstableview». Вы найдете образец исходного кода под названием «TableViewVariableRowHeights / TableViewVariableRowHeightsAppDelegate.m».

(Примечание: я смотрю на столбец 1 в виде таблицы, вам придется настроить, чтобы искать в другом месте)

в Delegate.h

IBOutlet NSTableView            *ideaTableView;

в Delegate.m

табличное представление делегирует управление высотой строки

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

вам также нужно это, чтобы изменить высоту новой строки (делегированный метод)

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

Надеюсь, это поможет.

Последнее замечание: это не поддерживает изменение ширины столбца.

Лоран
источник
4
К сожалению, этот пример кода Apple (который в настоящее время помечен как версия 1.1) использует таблицу на основе ячеек, а не таблицу на основе представления (о которой идет этот вопрос). -[NSTableView preparedCellAtColumn:row]Вызов кода , который, как сказано в документации, «доступен только для табличных представлений на основе NSCell». При использовании этого метода в таблице на основе представления журнал выводится во время выполнения: «Ошибка NSTableView на основе представления: была вызвана подготовленнаяCellAtColumn: row:. Пожалуйста, зарегистрируйте ошибку с обратной трассировкой из этого журнала или прекратите использование метода».
Эшли
1

Вот решение, основанное на ответе JanApotheker, измененном, поскольку оно cellView.fittingSize.heightне возвращало для меня правильную высоту. В моем случае я использую стандарт NSTableCellView, NSAttributedStringдля текста textField ячейки и таблицу с одним столбцом с ограничениями для textField ячейки, установленными в IB.

В моем контроллере представления я заявляю:

var tableViewCellForSizing: NSTableCellView?

В viewDidLoad ():

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

Наконец, для метода делегата 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- константа, равная 30, для резервного копирования, но на самом деле никогда не использовалась. attributedStringsэто мой модельный массив NSAttributedString.

Это отлично подходит для моих нужд. Спасибо за все предыдущие ответы, которые указали мне правильное направление решения этой досадной проблемы.

Джбарага
источник
1
Очень хорошо. Одна проблема. Таблица не может быть редактируемой или textfield.fittingSize.height не будет работать и вернет ноль. Установить table.isEditable = false в viewdidload
jiminybob99
0

Это очень похоже на то, что мне приходилось делать раньше. Хотел бы я сказать вам, что я придумал простое и элегантное решение, но, увы, я этого не сделал. Но не из-за отсутствия попыток. Как вы уже заметили, необходимость UITableView знать высоту до построения ячеек действительно заставляет все это казаться довольно круглым.

Моим лучшим решением было передать логику в ячейку, потому что, по крайней мере, я мог изолировать, какой класс нужен, чтобы понимать, как расположены ячейки. Метод вроде

+ (CGFloat) heightForStory:(Story*) story

сможет определить, какой высоты должна быть камера. Конечно, это включало измерение текста и т. Д. В некоторых случаях я изобретал способы кэширования информации, полученной в ходе этого метода, которую затем можно было использовать при создании ячейки. Это было лучшее, что я придумал. Это раздражающая проблема, поскольку, похоже, должен быть лучший ответ.

бродяга
источник