NSTreeControllerとCoreDataのバインディング

この記事は、macOS Catalina 10.15、Xcode 11.1で作成し、
macOS Big Sur 11.1、 Xcode 12.3で検証しています。

初心者ですが、macOSでCoreDataアプリを開発しようとしています。
NSTreeControllerの使い方を調べて試行錯誤した結果をメモしておきます。

## 新規プロジェクトの作成

新規にmacOSのアプリを作成します。

Project Name: NSTreeControllerTutorial
User Interface: Storyboard
Use Core Dataにチェックを入れます。

プロジェクトファイルの保存場所は、「書類」以外が良さそうです。
今回は関係ありませんが、SwiftUIのチュートリアルを試していたときに原因不明のエラーが出ました。

~~最初に表示されるTargetsの内容で、Infoタブを選択して、次のKeyの値をNOに変更します。~~

| Key | Value |
|:--|:--|
| Application can be killed immediately when user is shutting down or logging out  |NO|
| Application can be killed to reclaim memory or other extraordinary circumstances  |NO|

~~これがYESのままだと、アプリ終了時にCore Dataの変更内容が保存されないようです。~~

## ViewControllerにmanagedObjectContextのプロパティを作成

StoryboardでNSTreeControllerからバインドするためにViewController.swiftにmanagedObjectContextのプロパティを作成します。
ついでに、名前昇順でソートするためのSortDescriptorも併せて作成しておきます。
これらのイニシャライザも作成します。

```Swift:ViewController.swift
    // MARK: - Core Data
    
    @objc let managedObjectContext: NSManagedObjectContext
    
    @objc let sortByName: [NSSortDescriptor]

    required init?(coder: NSCoder) {
        self.managedObjectContext = (NSApp.delegate as! AppDelegate).persistentContainer.viewContext
        
        self.sortByName = [NSSortDescriptor(key: "name", ascending: true)]
        super.init(coder: coder)
    }
```

## CoreDataのModelを作成

Person Entityを作成します。

| Attribute | Type    |
|:----------|:--------|
|name       |String   |

| Relationship | Destination | Inverse | Type   |
|:-------------|:------------|:--------|:-------|
|children      |Person       |parent   |To Many |
|parent        |Person       |children |To One  |

![001.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225822/df888565-3e12-83f4-3b2b-057006b4a89b.jpeg)

## Storyboardに部品を配置

Storyboardに部品を配置します。
- Source List
- Outline View
- Tree Controller
- Push Button x3

動作の違いを見るために、Source ListとOutline Viewの両方を配置しました。

<img width="855" alt="002.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225822/2410a6e1-b03c-dfb8-8a3a-ce675b3040f0.png">

## NSTreeControllerの設定とバインディング

### Attributes inspector

| Key            | Value        | 備考                   |
|:---------------|:-------------|:----------------------|
|Children        |children      |relationshipsのchildren |
|Mode            |Entity Name   |                        |
|Entity Name     |Person        |                        |
|Fetch Predicate |parent == nil |relationshipsのparent   |

- [x] Prepares Content

<img width="254" alt="スクリーンショット 2019-10-22 15.07.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225822/6d9a5ae8-9487-d473-a9e7-923fca4c9d33.png">

### Bindings inspector

|                      | Bind to          | Model Key Path        |
|:---------------------|:-----------------|:----------------------|
|Managed Object Context|View Controller   |managedObjectContext   |
|Sort Descriptor       |View Controller   |sortByName             |

<img width="255" alt="スクリーンショット 2019-10-22 15.15.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225822/29c225d5-9137-2ab3-3004-50a38938bb20.png">

## Outline Viewの設定とバインディング

一つ目のTable ColumnのValueにTree Controllerをバインドします。
Headerで昇順・降順を切り替えないのなら、Value Key Pathのバインドは空欄で良さそうです。
今回は、昇順・降順を切り替えないので、Create Sort Descriptorのチェックを外しておきます。

- [ ] Create Sort Descriptor

なお、Value Key Pathをnameとバインドした上でCreate Sort Descriptorにチェックを入れておくと、実行時にタイトル行をクリックして昇順・降順を切り替えることができます。ただし、Tree ControllerのSort Descriptorsのバインディングを行っているとエラーが生じるようです。

<img width="1124" alt="スクリーンショット 2019-10-22 15.21.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225822/af363bd8-eea5-0a35-38e0-c348368664e6.png">

View Basedなので、直下のL Table View CellをTable Cell ViewのobjectValue.nameにバインドします。
nameを直接入力したいので、Conditionally Sets Editableにチェックをいれておきます。

- [x] Conditionally Sets Editable

<img width="1512" alt="スクリーンショット 2019-10-22 15.29.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225822/d2c25d27-1fea-787a-082a-b872fbe96f0c.png">

## Push Buttonのバインディング

ボタンは、Tree ControllerのAdd:、Add Child:、Remove:とバインドします。
Removeボタンは、EnabledをTree ControllerのcanRemoveともバインドしておくといいですね。

## 実行
<img width="592" alt="スクリーンショット 2019-10-22 15.45.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225822/6c45a468-e0da-2028-b05e-40b76ee01779.png">

とりあえず、ここまで動作しました。
アプリを終了して再起動すると、保存されたデータが名前順でソートされて表示できています。
２列目は設定を加えていないので、View Basedの初期表示ですね。

Source Listは、同様にバインドするだけでは表示できなくて、Outline View Delegateのfunctionを設定する必要があります。

## Source Listのバインディング

とりあえず、Source Listも同様にTable ColumnとTable View Cellのバインディングを設定します。

<img width="1124" alt="スクリーンショット 2019-10-22 16.45.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225822/418f74d5-5705-16e7-ff07-9dd8662f9f4c.png">
<img width="1123" alt="スクリーンショット 2019-10-22 16.49.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225822/e764b1bb-41eb-acac-f6a5-85020d9ff262.png">

実行してみると、一応動作はしているのですが、文字が表示されていません。

<img width="592" alt="スクリーンショット 2019-10-22 16.51.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225822/bb72dd49-4fa3-6e7b-de5a-c92d9a5d4cab.png">

## NSOutlineViewDelegateの設定

事前準備として、nameを表示したいTable View Cellを含むTable Cell Viewのidentifierを確認しておきます。
デフォルトで"DataCell"となっていました。
<img width="1128" alt="スクリーンショット 2019-10-22 18.48.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225822/07158b88-af7d-3756-6ad6-7e0360354607.png">


そこで、View ControllerにNSOutlineViewのデリゲートメソッドを追加します。
せっかく覚えたので、extensionで追加します。

```Swift:ViewController.swift
extension ViewController: NSOutlineViewDelegate {

    func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
        return outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("DataCell"), owner: self)
    }
    
}
```

デリゲートメソッドなので、Source ListのdelegateをView Controllerと接続する必要があります。
<img width="246" alt="スクリーンショット 2019-10-22 18.56.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225822/1830831e-0b59-4ae1-61ff-6da17371fdbf.png">

## 実行２

きちんと表示されました。これで完成！
<img width="592" alt="スクリーンショット 2019-10-22 18.59.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225822/9a6e521f-ae1f-89b4-76b1-e4f988ab914b.png">

## 参考としたサイト
記事作成に当たり、次のサイトを参考としました。

StoryboardでのManagedObjectContextとのバインディング
[macOSアプリ開発でStoryboard＋Core Dataを使うときのハマり話](http://hylom.net/macOS-app-develop-with-storyboard-and-coredata "macOSアプリ開発でStoryboard＋Core Dataを使うときのハマり話")

NSTreeControllerの使い方サンプル
[The simplest NSTreeController example](http://daemonconstruction.blogspot.com/2012/03/simplest-nstreecontroller-example.html "The simplest NSTreeController example")

Source Listの表示のためのデリゲートメソッド
[Binding view-based NSOutlineView to Core Data](https://stackoverflow.com/questions/7095703/binding-view-based-nsoutlineview-to-core-data "Binding view-based NSOutlineView to Core Data")