Swift で「Mac OSX Cocoa プログラミング」その4


ヒレガス本こと「Mac OSX Cocoa プログラミング」の例題を Swift で書いてみるメモ。その４回目。

今回は８章「NSArrayController」に挑戦してみる。この章では「RaiseMan」という従業員の予定昇級率の管理アプリケーションの作成を通して、NSArrayController を利用した Cocoa のMVC(Model-View-Controller)モデルについて学習する。

完成したアプリケーションは下図のようになる。

![スクリーンショット 2014-11-19 22.49.12.png](https://qiita-image-store.s3.amazonaws.com/0/31482/03ca6cad-3c62-1970-81b7-41abd6126618.png "スクリーンショット 2014-11-19 22.49.12.png")

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

XCode で新たな「Cocoa Application」プロジェクトを作成する。Product名を指定し、Language に Swift、「Use Storybords」と「Create Document-Based Application」にチェックを入れておく。

原書では、当然 Storybord を使用していないが、今時の Cocoa プログラミングということで今回は使用することにした。Document-Based Application は、いわゆるワープロや表計算ソフトのような文書ファイルを開いて、編集して保存するというスタイルのアプリケーションのことで、「Create Document-Based Application」にチェックを入れると、XCode が自動的にそういう雛形を作成してくれる。

### Person クラスの作成

モデルとなる Person クラスを作成する。
一人の従業員を表し、プロパティには名前と昇級率だけをもつ単純なクラスだ。オーバーライドしている setNilValueForKey というメソッドはキー値コーディングで Nil が設定された場合に呼び出されるメソッド。

```swift:Person.swift
import Cocoa

@objc(Person)
class Person: NSObject {
    var personName = "New Person"
    var expectedRaise : Float = 0.5
    
    override func setNilValueForKey(key: String) {
        if key == "expectedRaise" {
            expectedRaise = 0.0
        } else {
            super.setNilValueForKey(key)
        }
    }
}
```

クラス定義の直前にある

```
@objc(Person)
```

は、Swift のクラス名をそのまま Cocoa Binding で利用するためのおまじない。詳しくは以下のリンクを参照。

[Swift + Cocoa Bindingで気をつけること - Qiita](http://qiita.com/_tid_/items/b7fe721f7178349f512a)
[osx - Swift – "Cannot find object class with name" - Stack Overflow](http://stackoverflow.com/questions/24215355/swift-cannot-find-object-class-with-name)

### Document クラスの修正

Document クラスは自動生成された NSDocument の派生クラスで、このアプリケーションで扱う文書データを示すクラス。アプリケーションで新規の文書が作成されると、Document クラスのインスタンスが一つ生成される。インスタンスの生成はフレームワークが自動でやってくれるので、プログラムでは特に何もする必要がない。

作成する RaiseMan アプリケーションは一つの文書に複数の従業員の昇級率を保持する必要があるので、Document クラスのメンバに先ほど作成した Person クラスの配列を追加する。

```swift:Document.sift
import Cocoa

class Document: NSDocument {

	// 中略 ...

    var employees : [Person] = []
    
}
```

原書では NSMutableArray を用いており参照型、それに対して Swift の配列は構造体で値型。問題ないんだろうかという気がしないでもないが、そこら辺は互換性がとれるように巧くやってくれているだろう。

### ViewController クラスの修正

自動生成された、ViewController クラスで、viewWillAppear メソッドをオーバーライドする。

```swift:ViewController.swift
import Cocoa

class ViewController: NSViewController {

	// 中略
	
    override func viewWillAppear() {
        let wc = self.view.window!.windowController() as NSWindowController
        let doc = wc.document as Document
        self.representedObject = doc
    }

}
```

このコードは原書の例題には存在しない。そもそも原書の例題には ViewController クラスなど存在しないし。
今回、StoryBord を採用したため、そのために必要となったプログラムの変更。
やっていることは、生成された Document クラスのインスタンスを ViewController クラスの self.representedObject へのセット。最初、viewDidLoad() でやってみたが、この時点ではまだ View の Window プロパティに値がセットされていなかった。

### ArrayController の追加

ここからは Storybord での作業になる。逆にいえば Swift のコードは先ほどでお終い。Swift の勉強には全然ならないな。

Object Library から ArrayController を ViewController Scene にドロップして追加する。
次に、追加した ArrayController を選択して、Attribute Inspector の Object Controller 欄を以下のように設定する。

![スクリーンショット 2014-11-20 0.10.11.png](https://qiita-image-store.s3.amazonaws.com/0/31482/d21daa78-d23d-c034-d6ba-13b65cfe7915.png "スクリーンショット 2014-11-20 0.10.11.png")

次に、Binding Inspector の Controller Content 欄を以下のように設定する。

![スクリーンショット 2014-11-20 0.13.11.png](https://qiita-image-store.s3.amazonaws.com/0/31482/55ac1621-27b2-623f-ba0f-8f584741389b.png "スクリーンショット 2014-11-20 0.13.11.png")

ここ、原書ではバインド先は File's Owner にせよとなっているのだが、今回、Storybord を利用しているため、ここで File's Owner を選択することができない。Storybord を利用する場合（というか、ViewController を利用する場合といったほうが正確か？）、View Controller を指定し、representedObject 経由でアクセスするのが定番のようだ。（先ほどの View Controller へのコードの追加はそのため）

### View へのUI部品の追加とバインドの設定

あとは、Storybord 上で ViewController に完成図のように Table View やら、Push Button などを貼り付け、属性やらバインドの設定を原書の例題にそって適宜設定していけば完成。

NSTableView への Cocoa バインドの設定だが、原書の例題では Content Mode を「cell based」で行うようになっている。（というかて手元の第３版の XCode3.2 の時代には「cell based」しか存在しなかった）だが、NSTableView の cell based は互換性のために残されているだけなので、今回は Content Mode を現在のデフォルトである「view based」にしている。そのため、ArrayController との Cocoa バインディングの設定箇所が原書の例題とは異なる。

まず、TableView を選択し、Binding Inspector より下図のように

![スクリーンショット 2014-11-20 0.46.32.png](https://qiita-image-store.s3.amazonaws.com/0/31482/d752e967-0bfc-3102-10ed-29affe720878.png "スクリーンショット 2014-11-20 0.46.32.png")

Table Contnt に Array Controller をバインドする。

次に、Table View Cell を選択し、Binding Inspector より Value に Person クラスの各プロパティをバインドする。

![スクリーンショット 2014-11-20 0.49.51.png](https://qiita-image-store.s3.amazonaws.com/0/31482/d0f131aa-1496-49ac-ee35-a9cc13fa7c38.png "スクリーンショット 2014-11-20 0.49.51.png")

この時、バインド先は Table Cell View、Model Key Path には objectValue.[プロパティ名」という形式で記述する。

数値項目に対する Number Formatter の適用などは cell based の場合と特に変わりはない。

### 課題その１：名前欄を空欄にすると落ちる

アプリケーションを実行して、Add New Employee ボタンをクリックしてデータを追加。名前欄をクリックして編集状態にして内容を削除して空欄にすると、EXE_BAD_ACCESS で落ちる。原因がさっぱり判らない。

[Swift Bindings won't work Xcode 6 Beta 5 - Stack Overflow](http://stackoverflow.com/questions/25269436/swift-bindings-wont-work-xcode-6-beta-5)

上記リンクと同じ現象なのかな？

#### 追記：解決した（2014.11.21)

空欄を入力すると文字列であっても空文字ではなく、Nil が入力されるようだ。バインドしている personName の型が String のため Nil を代入することができず EXE_BAD_ACCESS が発生していたもよう。以下のように

```swift
var personName : String? = "New Person"
```

personName もオプショナル型である String? として定義してやることでエラーを回避することができる。
文字列の場合、NSString が Nil の代入を許しているため setNilValueForKey は呼ばれないのだな。気付くのに時間がかかってしまった。

### 課題その２：Table View の並びかえ

原書では章の最後に、Table View の並びかえ機能を実装している。といっても、Interface Builder でカラムの属性を設定するだけなのだが、今のところ Swift を利用した場合の設定方法が判らない。

![スクリーンショット 2014-11-20 23.30.23.png](https://qiita-image-store.s3.amazonaws.com/0/31482/23ffc67a-fd01-5c7d-76ae-8ceb4a762fc7.png "スクリーンショット 2014-11-20 23.30.23.png")

見よう見まねで上図のように NSTableColumn に設定してみたのだが巧く動かない。設定方法が悪いのか？　それとも、personName が NSString でないのがダメなのか？