NSTableViewの行の高さを、コンテンツの内容に合わせて変更する


概要

  • 下記のようにウィンドウの大きさに関わらず、テキストが全文表示されるようにしたいです。
  • そのためにはテーブルの行の高さを都度変更しないといけません。
  • macOS 10.13から簡単に設定できるようになっていたので、その方法とそれを使わない方法の2つを紹介します。

GitHub

参考

はじめは、
 tableView.makeViewWithIdentifier(tableColumn!.identifier, owner: self) as! NSTableCellView
 でCellViewを取得して、これのtextField.bounds.heightを使えばいいんじゃないかと思ったんですけどダメでした。この高さ、テキスト折り返した後も何にも変わってない。
 正解は
 textField.attributedStringValue.boundingRectWithSize(CGSizeMake(
CellView.frame.width, CGFloat.max), options: .UsesLineFragmentOrigin, context: nil)
 とやって領域情報(NSRect)を取得して、これのheightを使えばよいみたいです。

実装例

1. NSTableViewの設定で簡単に

解説

  • Auto Layoutを設定してやりRow Size StyleAutomatic(Auto Layout)を設定することで、自動でコンテンツに合わせた行の高さにすることができます。
  • View-based NSTableView with rows that have dynamic heights
    • macOS 10.13から実装されたそうで、非常にありがたいです。

This got a lot easier in macOS 10.13 with .usesAutomaticRowHeights

  • 横幅の下限・上限は下記より設定できます。

つまづきポイント

  • よくわからない点ですが、テキストオブジェクトをLabel(Libraryボタンから導入)で置き換えると文章が折り返しされません。

  • 下記をインポートし、クラスをinit(これは何でしょうか?)からNSTextFieldに変更して使うとうまくいきました。
  • 設定値は同じ用にしたつもりだが、何か見落としていたのかもしれませんね。

2. 行の高さを計算する

解説

  • NSTableViewDelegateoptional func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloatというのがあり、これで各行の高さを指定することができます。
  • 行の高さの計算方法は、泥臭いですがNSTableViewの幅からNSTextFieldの高さを算出し、Constraintsの値を考慮して算出します。
  • 肝心のNSTextFieldの高さの求め方ですが、行数は固定ではなく、幅によって折返しがあるのでそこを考慮する必要があります。
  • そこでboundingRectを使用します。
    • テキストを表示する範囲を指定して、実際に描画される範囲が取得できます。
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
    let cellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "FirstColumn"), owner: self) as! SecondViewControllerTableCellView
    cellView.nameTextField.stringValue = String(format: "%d番目の人", row)
    cellView.messageTextField.stringValue = messageList[row]    // 計算用

    let widthOfMessageTextField = cellView.calculateWidthOfMessageTextField(tableView.frame.width)
    let widthOfNameTextField = cellView.calculateWidthOfNameTextField(tableView.frame.width)
    // 指定範囲でのNSTextFieldの描画予定範囲を取得し、そこからテーブルに必要な高さを計算する
    let messageTextFieldRect = cellView.messageTextField.attributedStringValue.boundingRect(with: CGSize(width: widthOfMessageTextField,
                                                                                                         height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil)

    let nameTextFieldRect = cellView.messageTextField.attributedStringValue.boundingRect(with: CGSize(width: widthOfNameTextField,
                                                                                                      height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil)

    return cellView.nameTextFieldTopConstraint.constant
        + nameTextFieldRect.height
        + cellView.messageTextFieldTopConstraint.constant
        + messageTextFieldRect.height
        + cellView.messageTextFieldBottomConstraint.constants   
}
  • またNSTableCellViewのカスタムクラスを作成し、Constraintsの値をとるために@IBOutletを定義しています。
class SecondViewControllerTableCellView: NSTableCellView {
    @IBOutlet var myImageViewHeightConstraint: NSLayoutConstraint!
    @IBOutlet var myImageViewWidthConstraint: NSLayoutConstraint!

つまづきポイント

  • cellViewの大きさを使えばいいのでは?と思っていたのですが、このメソッド内で取れる大きさはあくまでIB上で表示されている設定値で、実際の大きさではないようでした。
    • optional func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?内では実際のサイズが取得できてそうでした。
  • NSTableCellView のサイズを自動的に調整させてみる

実際のところは、テキストを設定してもすぐには frame にサイズが反映されなくて、View の fittingSize を呼び出すことで、テキストの量に応じたサイズを取得できる様子でした。

  • ↑がうまく取れませんでした…。
  • またNSTextFieldsizeToFit()も折り返しを考慮しない場合の値となるので今回の使用には適していません。