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

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

<img src="https://i.imgur.com/7eUrB8U.jpg" width=900%>

<img src="https://i.imgur.com/NudPfc3.jpg" width=50%>

# GitHub

- https://github.com/pommdau/Qiita_ChangeableHeightTableView

# 参考
- [XcodeでCocoa, Swiftを勉強　セルの高さをセル内のテキストの高さに調整する](https://www.kilinbox.net/2016/05/xcodecocoa-swiftview-cellviewheight.html)
    - 肝のアイディアはこちらから。
    - 上の記事ではAutoLayoutは使っていないので補完します。

> はじめは、
> 　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を使えばよいみたいです。

- [tableViewColumnDidResize\(\_:\)](https://developer.apple.com/documentation/appkit/nstableviewdelegate/1535901-tableviewcolumndidresize)
    - テーブルサイズが変更されたときに`reloadData`を呼ぶことで、サイズを変更した際に対応します
- [Macアプリ初心者：NSTableView でカスタムした一覧を作ってみる](https://qiita.com/ynozue/items/da96d8bf1a2e2e1f3da0)
    - カスタムした`TableCellView`クラスの使用方法
- [Auto\-resizing NSTokenField with constraint\-based layout](https://stackoverflow.com/questions/17147366/auto-resizing-nstokenfield-with-constraint-based-layout)
    - `cellSizeForBounds`も使えそう？
- https://lipsum.sugutsukaeru.jp/index.cgi
    - ダミーテキスト作成に使用しました

# 実装例
## 1. NSTableViewの設定で簡単に
### 解説
- Auto Layoutを設定してやり`Row Size Style`で`Automatic(Auto Layout)`を設定することで、自動でコンテンツに合わせた行の高さにすることができます。
- [View\-based NSTableView with rows that have dynamic heights](https://stackoverflow.com/questions/7504546/view-based-nstableview-with-rows-that-have-dynamic-heights/47065227)
    - macOS 10.13から実装されたそうで、非常にありがたいです。

> This got a lot easier in macOS 10\.13 with \.usesAutomaticRowHeights

![-w281](https://i.imgur.com/zgL0ZlR.jpg)

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

![-w1420](https://i.imgur.com/JCPpCL5.jpg)

### つまづきポイント

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

<img src="https://i.imgur.com/x5oxI55.jpg" width=50%>

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

<img src="https://i.imgur.com/UCAKEKO.jpg" width=50%>

## 2. 行の高さを計算する
### 解説
- `NSTableViewDelegate`に`optional func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat`というのがあり、これで各行の高さを指定することができます。
- 行の高さの計算方法は、泥臭いですが`NSTableView`の幅から`NSTextField`の高さを算出し、`Constraints`の値を考慮して算出します。
- 肝心の`NSTextField`の高さの求め方ですが、行数は固定ではなく、幅によって折返しがあるのでそこを考慮する必要があります。
- そこで`boundingRect`を使用します。
    - テキストを表示する範囲を指定して、実際に描画される範囲が取得できます。

```swift
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`を定義しています。

```swift
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 のサイズを自動的に調整させてみる](https://ez-net.jp/article/00/jVzOXcdJ/C603m85YfWFm/)

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

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