NSOutlineViewの基本的な実装

# 概要
- `NSOutlineView`を使って下記のように表示するだけの実装を行う。

<img width="256" alt="image" src="https://i.imgur.com/R6d7CVG.jpg">

# GitHub

- https://github.com/pommdau/nsoutlineview-simple-demo

# 参考
- [NSOutlineView on macOS Tutorial](https://www.raywenderlich.com/1201-nsoutlineview-on-macos-tutorial)
    - 主に参考とした
- [NSOutlineView 開閉時の挙動を比較してみる \- Qiita](https://qiita.com/usagimaru/items/51bcdd45d7a3ab8fed26)
- [HIG: Outline Views](https://developer.apple.com/design/human-interface-guidelines/macos/windows-and-views/outline-views/)
- [公式ドキュメント: NSOutlineView](https://developer.apple.com/documentation/appkit/nsoutlineview)

# 実装
## Model

- 今回セルに対応されるModelは以下の通り。
- `NodeType`はそのセルが`Group`なのかを判断するのに使用している

```swift
enum NodeType {
    case group
    case parent
    case item
}

class Node {
    
    // MARK: - Properties
    
    var title: String
    var nodeType: NodeType
    var children: [Node]
    
    // MARK: - Lifecycle
    
    init(title: String,
         children: [Node] = [],
         nodeType: NodeType = .item) {
        self.title = title
        self.children = children
        self.nodeType = nodeType
    }
    
    // MARK: - Helpers
    
    var numberOfChildren: Int {
        children.count
    }
    
    var hasChildren: Bool {
        !children.isEmpty
    }
}
```

## ViewController.swift
### NSOutlineViewの設定

- 下記の通り`NSOutlineView`に必要な設定をコード内で行う
- `selectionHighlightStyle`や`floatsGroupRows`の設定で見た目と挙動が大きく変わるので、下記を参考に必要なものを選択すると良い
    - [NSOutlineView 開閉時の挙動を比較してみる \- Qiita](https://qiita.com/usagimaru/items/51bcdd45d7a3ab8fed26)

```swift
private func initializeOutlineView() {
    // 別のXibファイルから読み込む場合は登録が必要
    outlineView.register(MyCellView.nib(inBundle: nil),
                         forIdentifier: NSUserInterfaceItemIdentifier(rawValue: String(describing: MyCellView.self)))
    outlineView.delegate = self
    outlineView.dataSource = self
    outlineView.autosaveExpandedItems = true
    
    // スタイルの設定
    outlineView.selectionHighlightStyle = .regular
    outlineView.floatsGroupRows = false
    
    nodes.append(contentsOf: Node.createSampleNodes())
    outlineView.reloadData()
}
```

- 全列を開閉状態にする
    - [NSOutlineView \- Auto\-expand all nodes](https://stackoverflow.com/questions/519751/nsoutlineview-auto-expand-all-nodes)
    - [expandItem:expandChildren:](https://developer.apple.com/documentation/appkit/nsoutlineview/1532826-expanditem?language=objc)

>Starting in OS X version 10.5, passing 'nil' will expand each item under the root in the outline view.

```swift
outlineView.expandItem(nil, expandChildren: true)
```

### NSOutlineViewDataSource

- `NSOutlineView`のデータに関するDelegateメソッドを実装する
- itemが含むchildrenの数を返す

```swift
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
    if let node = item as? Node {
        return node.numberOfChildren
    }
    
    return nodes.count
}
```

- 親に対する子の情報を与える

```swift
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
    if let feed = item as? Node {
        return feed.children[index]
    }
    
    return nodes[index]
}
```

- itemが開閉可能か（trueを返すとCellViewにDisclosure Buttonが表示される）

```swift
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
    if let node = item as? Node {
        return node.hasChildren
    }
    
    return false
}
```

- itemがグループ行のものかどうか

```swift
// グループ行かどうか（NSOutlineViewの設定如何では見た目と挙動が特別となる）
func outlineView(_ outlineView: NSOutlineView, isGroupItem item: Any) -> Bool {
    guard let node = item as? Node else {
        return false
    }
    
    return node.nodeType == .group
}
```

### NSOutlineViewDelegate

- `NSOutlineView`のDelegateメソッドを実装する
- 各行に表示するCellViewの設定

```swift
public func outlineView(_ outlineView: NSOutlineView,
                        viewFor tableColumn: NSTableColumn?,
                        item: Any) -> NSView? {
    
    guard
        let myCellView = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: String(describing: MyCellView.self)), owner: self) as? MyCellView,
        let node = item as? Node
    else {
        return nil
    }
    
    myCellView.configureUI(withNode: node)
            
    return myCellView
}
```

- 行選択が変更された場合に呼ばれるNotification

```swift
func outlineViewSelectionDidChange(_ notification: Notification) {
    guard let outlineView = notification.object as? NSOutlineView else {
        return
    }
    
    let selectedIndex = outlineView.selectedRow
    guard let node = outlineView.item(atRow: selectedIndex) as? Node else {
        return
    }
    
    print("Selected Title: \(node.title)")
}
```

# NSTreeControllerの話
- `NSOutlineView`で調べていると`NSTreeController`を使っている例が多くヒットする。
- ただ`CocoaBinding`が絡んでいて、私の理解が浅いためちょっと使いこなせなかった。
    - ので今回は`NSOutlineView`単体で実装している
- イメージに過ぎないが、`NSTreeController`を使うことで煩雑な処理、例えば複数選択のアクション・入れ替え・削除が簡単に扱えると思っている。

## 参考

- [Navigating Hierarchical Data Using Outline and Split Views](https://developer.apple.com/documentation/appkit/cocoa_bindings/navigating_hierarchical_data_using_outline_and_split_views)
    - Appleのしっかりとした（珍しい）macOSのチュートリアル
- [NSTreeController](https://developer.apple.com/documentation/appkit/nstreecontroller)
- [The simplest NSTreeController example \| Daemon Construction](http://daemonconstruction.blogspot.com/2012/03/simplest-nstreecontroller-example.html)
    - 何がなんだかわからんけど、なぜか動くサンプル
- [NSTreeController \+ NSOutlineView: A powerful combination \| by Alexander Murphy \| Building Ibotta \| Medium](https://medium.com/building-ibotta/nstreecontroller-nsoutlineview-a-powerful-combination-ca17fd233139)
    - コードベースのバインディングを行っていて比較的理解はしやすい
- NSTreeControllerを使うため、バインディングの基本を知りたいと思い…
    - [Cocoa Bindings on macOS](https://www.raywenderlich.com/921-cocoa-bindings-on-macos)
- [How to Use Cocoa Bindings and Core Data in a Mac App](https://www.raywenderlich.com/2814-how-to-use-cocoa-bindings-and-core-data-in-a-mac-app)
    - CoreDataとバインディング。まだ見れていない。
- [NSTreeControllerで使用するObjectの変換についてのメモ — MindTo01s](https://www.mindto01s.com/2017/02/21/f42c08e5_31e7_462d_95db_a43283dae235.html)
    - NSTreeControllerとNSOutlineviewの相互変換の話