プログラミング

Application開発

2014年

Swift

一時の危機的状態から飛ぶ鳥を落とす勢いに駆け上がったAppleが開発言語という非常に地味なものを開発していた,というのは驚きであった。iPhone・iPad専用言語としてでも十分生き残れそうな気もするが,Cocoa・Objective-Cを発展させるため「Swift」をもってきたのだ。私はCもJavaもC++もRubyも知らない完全に独学のプログラマーである。それまでは,ExcelのVisual Basic for ApplicationとAppleScriptでのプログラミングしか経験がなかったわけだが,Cocoa・Objective-Cでも「とりあえず作ってみる」というのはそれ程困難ではなかった。強いて言うならばObjective-C解説の本が少ないこと,当時は参考になるホームページも少なかったことだ。

今は状況がかなり変わっている。iPhoneのおかげでObjective-C・Cocoaに関する本は激増している。私自身はScatterMaker1.0を作った最初に,Cに関するもの一冊,Objective-C・Cocoaを二冊買って以来何も買っていないが。

Ver1からのアップデートから8年以上,まだ動いているのが奇跡と言えるくらいの長寿となったVer. 2.0。開発時は丁度Intelマシンが出てきたところ,自分はIntelマシンは持っていなかったが,最終コンパイル寸前で当時でいう「Universal Binary」化(Intel CPUで動作するバイナリとPower PCで動作するバイナリを両方もつアプリケーション,全く異なるCPUで動作する,というある意味奇跡の仕組,今のUniversalとは異なる意味)することができた。そのほんの僅かな差で長寿アプリケーションとなった。

しかし,本当に完成するかどうかも分からない状態で試しに作ったVer1.0を改変しただけだったので,基本構造はお粗末だった。「できてしまった」というのが正直なところだった。そこでVer3.0はまず「普通の」アプリケーションにするところをベースにした。

何となくできてしまったVer1.0の経験で何とかなると思って始めたが,ここからが苦難の始まりだった。ファイルを書類として扱える普通のアプリケーションにしたいと思って改変を進めた。しかし,全体を管理できているつもりだったが,どんどん制御系が複雑になり,遂には出てくる不具合を修正できなくなった。基本的なアルゴリズムは全く変えていないのにどんどん修正はやっかいになり,休憩のつもりでお休み期間を置くとますます細部の構造を忘れてしまい,最後には3.0はお蔵入りとなった。

Swiftも非常に分かりやすい言語である。Objective-Cを少しでも知っていれば比較的簡単に書ける。Objective-Cを知っていれば簡単に書ける,というだけであれば移行する意味は全くない。素人プログラマにとって最もありがたいのはコードが短くなることではないだろうか。単純に短くなるだけでも,コードの視認性が良くなって,全体が見渡し安くなる。ヘッダーをいちいち編集しなくてもよいのもありがたい。3.0のように頓挫しなかったのはSwiftのおかげであろうと思う。

しかし,Swiftに関するホームページは増えてきたが,まだまだ日本語のページは少ない。しかも,英語のものも含めてほとんどがiOSに関するものなので,OS Xに関するものでしかも日本語のページは非常に少ない。今時OS XのApplicationを個人で開発している人はほとんどいないのかもしれない。

SwiftによるOS X Application開発で苦労したこと,やっと見つけた解決法などを書いてみようと思う。SwiftやCocoaの基本的概念などを語るのは私はやらない方がいいと思う。それは成書やきちんとプログラミングを分かっている人に譲る。プログラミングに関してちゃんと基礎から学んでいるわけではないので,笑ってしまうような誤解もあるかもしれないが,ご容赦いただきたい。もし誤解があったらできればやさしくそっと教えて欲しい。それでも,初心者が行き詰まりことが多くあると思うので,きっと無駄ではないと思う。

2004年

Objective-C

私の本職はコンピュータの関係ではありません。プログラミングを誰かに教わったこと,系統立って勉強したことも全くありません。すべて独学の趣味のサンデープログラマーです。プログラミングとの最初の出会いはアプリケーションのマクロを使っての自動化でした。うんざりするような繰り返しの作業をミスなく早くするためです。マクロを作っているよりも地道に手動でやった方が早いのではと葛藤しながらも,作っていました。

そうしてあの偉大なAppleScriptと出会いました。AppleScriptは登場したばかりで,単純な作業が中心でしたが,複数のアプリケーションにまたがった自動化プログラムを作ることができるのは革新的でした。AppleScript対応でないものも,メニュー選択をAppleScript対応にするユーティリティを使って自動化を実現しました。このころから,ユーティリティ的なマクロプログラムではなく,アプリケーションを作りたいという夢はありましたが,当時は情報もなく,作成のためのアプリケーションも高額であったため,ほとんど単なる夢想でした。

時代はMac OS Xとなり,そしてXcodeと出会いました。昔から頭の片隅にあった密かな密かな「アプリケーションを自作する」という夢想が現実味をおびてやってきました。挫折するのではという不安はありましたが,2,3冊の本を買ってアプリケーション作りに望みました。C言語というものも今回初めて学習しました。Xcodeがfreeでなければこんな冒険はできませんでした。Mac OSを愛してよかった,と再び三度思った瞬間です。

しばらくは時間もなくて「サンデープログラマー」とも呼べない状況でしたが,バグを修正するために久しぶりにコードを見直してみて,またこの世界に目覚めました。Xcode,Objective-Cはこの世界では非常にマイナーな分野のようで,職業的プログラマーとしては苦労されている方も非常に多いということですが,私個人的には非常にわかりやすく,理解しやすい言語のように感じています。これからもまたこつこつとやっていこうと考えています。

macOSアプリケーションのプログラミング - 表

2015年7月

ScatterMaker4の表においては一つの列に一種類のCellしか使えなくても全く困らない。しかし,二ヶ所の設定に使用しているアウトライン表 outlineViewにおいてはそれでは非常に使いづらい。また,Appleからのドキュメントをみても「Cell-based」は「legacy」であり,新しいプロジェクトには非推奨,となっている。よってScatterMakerにおいても「View-based」に切り替えていくことにした。まずoutlineViewからview-basedに移行を試みた。実装の仕方の違い,挙動の違いでかなり戸惑った。

TableViewCell上のTextField,Stepperの謎

今回はTableViewCell上で,NSTextField,NSButton,NSPopUpButton,NSColorWell,NSStepperを扱ってみた。いずれもサブクラスは作っていない。6月のところで書いたようにNSTextFieldとNSStepperは苦労した。他のパーツがすんなり動作しただけにかなり戸惑った。最も頻繁に使用するであろうNSTextFieldがなかなか設定できないのが本当に困ったし,不可解であった。何とか動作するようにはなったが,一部言うことを聞かない部分は残っており,完全に解決したわけではない。

確かにAppleのドキュメントには注意を促す記述がある。

Some controls, such as NSTextField, should only become first responder when the enclosing NSTableView/NSBrowser indicates that the view can begin editing. It is up to the particular control that wants to be validated to call this method in its mouseDown: method (or perhaps at another time) to determine if it should attempt to become the first responder or not.

しかし,ドキュメントには具体的にどうすればよいのか具体的には記載がない。具体例もない。検索をかけた結果,TableViewに

func validateProposedFirstResponder(_ responder: NSResponder, forEvent event: NSEvent?) -> Bool

と聞いたときに,ちゃんとtrueを返さないと動作をしないらしい。よって動作されるために,NSTableView/NSOutlineViewのサブクラスを作って,上のメソッドを上書きして,ちゃんと「true」と答えるようにしなければならない。このためにサブクラスを作らねばならない。

class MyOutlineView: NSOutlineView {

override func validateProposedFirstResponder(responder: NSResponder, forEvent event: NSEvent?) -> Bool {

return true

}

}

とたったこれだけのためのサブクラスを作るとNSStepperも動作するようになる。

まるでNSClipViewのcoordinateを反転させるのに変数の「flipped」を「true」に設定するのにわざわざサブクラスを作るのに似ている。

class MyClipView: NSClipView {

override var flipped: Bool {

get {

return true

}

}

}

という風に

NSTextField

値の表示はすんなりできた。しかし,ユーザ入力のコントロールがうまくいかない。何とか「editable」を制御することには成功したが,「enabled」は今でも制御できない。

TableViewの中では「enabled」を制御することがなかなかできない。値の設定でバインディングを使うとそれだけで「enabled」が設定できなくなる。「enabled」は制御できないのにeditableにするために「enabled」を設定しなければならない。つまり,editableにするためには,enabledもeditableも両方trueに設定する必要がある。バインディングで制御するには,NSTextFieldだけではなく,中のNSTextFieldCellにもバインディングを設定しなければならない。不可解な挙動である。

NSStepper

TableViewCell上にNSStepperのインスタンスを置いても表示はされるがデフォルトの状態では全く動作しない。

http://stackoverflow.com/questions/13108177/nsstepper-in-nstablecellview-has-no-effect

この記事にある通り,上に書いたようなコードを足すと動作するようになる。

しかし,バインディングはうまく動作しない。NSStepperに値を設定することはできるが,minValueやmaxValueを設定することができない。Appleのドキュメントではこれもバインディングで設定できるように見えるが,うまく動作しない。Delegateメソッド「.... viewAtColumn ....」の中でその都度設定しなければならない。

2015年6月

NSTableViewDataSource Protocol

optional func numberOfRowsInTableView(_ aTableView: NSTableView) -> Int

「何行あるか?」というだけのメソッド。Cell-basedでも共通であるし,直感的にも問題のないメソッド。

optional func tableView(_ aTableView: NSTableView, objectValueForTableColumn aTableColumn: NSTableColumn?, row rowIndex: Int) -> AnyObject?

「View-based」においては,一つのCellが一つのtableCellView(NSTableCellViewまたはそのサブクラス)に対応する。上のメソッドで返される値はこのtableCellViewの値,つまり(tableCellViewのインスタンス).objectValueの値となるようだ。TableCellViewは一つのviewなのでNSViewの上に置けるものであれば何でも置ける。そうすると,上で返される値を表示させるためにはどうすればよい?どのオブジェクトに表示させるか指定するにはどうする?という疑問が出てくる。

表示させたいオブジェクトとバインディングbindingさせる。バインディングさせるには,もちろん方法は2つ,Interface Builderでバインディングの設定をするか,programmaticalにbindさせるかである。Interface Builderではその列のデフォルトのCellしか設定できない。その他のCellはprogrammaticalに作らねばならないから,要するにどちらのやり方もできなければならない。Interface Builderでもデフォルトでは何にもバインディングしていない。手動で繋がなくてはいけない。これまで全くバインディングを使っていなかったので,一からバインディングの勉強が必要であった。自由度が高いだけにCell-basedよりもかなりややこしい。

話をさらにややこしくしているのは,Appleのドキュメントにも出てくるサンプルプログラムである。サンプルプログラムの中の少なくとも一つの表はこの「.... objectValueForTableColumn ....」の中ではなく,後述のNSTableViewDelegate Protocolの中の「.... viewAtColumn ....」で値を設定している。AppleのClass Referenceの中では,「.... objectValueForTableColumn ....」はバインディングを使わないのであれば,実装しなければならないことになっている。Delegateメリッドで値を設定するというやり方をしてよいのであれば,「.... objectValueForTableColumn ....」を実装する意味は何なのか。手動またはprogrammaticalにバインディングも設定しなくてはならないので,本当にこのDataSource Protocolの意義がわからない。

optional func tableView(_ aTableView: NSTableView, setObjectValue anObject: AnyObject?, forTableColumn aTableColumn: NSTableColumn?, row rowIndex: Int)

View-basedになって消えてしまったメソッド。View-basedではもう呼び出されない。上のメソッドでわざわざ値を設定させているのに,その値を書き換えた時に自動的に呼び出されるメソッドはない。いっそのこと上のメソッドも一緒に廃止するか,オプションでこの2つのメソッドを使えるようにする,という選択肢はなかったのだろうか。

NSTableViewDelegate Protocol

func viewAtColumn(_ column: Int, row row: Int, makeIfNecessary makeIfNecessary: Bool) -> AnyObject?

View-basedでは必須のメソッド。Cellのオブジェクト,一般的にはNSTableCellViewかそのサブクラスのインスタンスを返す。再表示させるために次々とtable cell viewを作っていては持たないので,できるだけCellを再利用する。再利用するには,

func makeViewWithIdentifier(_ identifier: String, owner owner: AnyObject?) -> AnyObject?

を使う。「owner」はNIBのowner。そう,このメソッドが呼び出される度に「awakeFromNib」が呼ばれるようだ。やはり,「awakeFromNib」に初期化プロセスを入れてはいけない,というのは本当であった。その列のデフォルトのCellはtableColumn.identifierと同一にし,別のタイプのtable cell viewを時々使う時には別のidentifierを使ってみた。それで作るとtable cell viewがストックされ,上のメソッドで再利用できるようだ。同じ種類のtable cell viewを使って,その上に乗せるものをこの中で変えてもよいだろう。乗せるオブジェクトが多ければ,または重たければ,再利用の観点から,一度作ったらオブジェクトを乗せたまま再利用できるようにidentifierを変えるのが良いだろう。

ここで混乱,困惑したのは2つ。上にも少し書いた,この値の設定にdelegateメソッドが顔を出してくる問題とtable cell viewに乗せるオブジェクトの「enabled」とか「editable」などの属性を設定する部分でのトラブルだ。

前者の問題はサンプルコードで,このdelegateメソッドの中で値を設定するくだりが出てくることである。Cell-basedの時は,それはするな,ということになっていたはずである。実際にここで値を設定しても正常に動作する。

Table cell viewに乗せるオブジェクト,多くはNSControlのサブクラスであろうが,によっては「enabled」「editable」などの属性を設定するのが非常にやっかいな場合がある。困ったのはNSTextField,NSStepperである。不可解な部分は多いが,色々試して何とか解決した。続きは次の記事で。

Table cell viewに乗せるオブジェクトに触った時に何をさせるかは,ここで設定するか,Interface Builderで設定するか二通りある。NSControlのサブクラスを乗せている場合はactionを設定すればよい。確かに複数のオブジェクトを乗せることができるので「.... setObjectValue ....」ではどうやっても対応できない場合があるので,仕方のない仕様かもしれない。

2014年12月

表には「Cell-based」と「View-based」がある。ScatterMakerにおいては,表は大事なインプットの場であるが,アウトプットとしてはそれほど活用しないので,表示に凝る必要はない。よってここの話はすべて「Cell-based」の話である。「Cell-based」には,同一の列においては異なるタイプのCellを使用することができない,などの制限がある。凝った表を作るには「View-based」で作る必要があるだろう。

もちろん表はScatterMaker1.0から作っている。必ず実装しなければならないdataSource protocolのメソッドの実装に成功すれば表示と手動でのデータの書き換えはできるようになる。そしてdelegate protocolを適宜実装すると概ね基本的な制御はできるようになる。しかしそれだけでは一つのセルを選択する,など普通の表計算ソフトができることがなかなかできない。

基本

NSTableViewまたは自前で作ったそのサブクラスを配置する。NSTableViewのインスタンスそのものはデータを持つ必要はなく,ただの「表示したり操作したりするための場」である。DataSourceに指定したオブジェクト(.xibファイル内で結んだ先かまたはprogrammaticalに指定した先)に何の値を表示させるかを聞いてそのオブジェクトが返して値を表示させる仕組みだ。表示や挙動に関してはdelegateに指定したオブジェクトに聞く。それは同一であっても構わないし,自前で作ったサブクラスを使っているのならば,自分自身(NSTableViewのサブクラス)をdataSourceやdelegateに指定してもよい。

Swiftの場合はファイルの最初の方,何のクラスを継承しているかの次にどのプロトコールに従っているかを書く。このおかげで推測入力ができるようになっている。もうSwiftも正式版になったので,そうそう急にメソッド名が変わったりはしないと思うが,betaの段階では時に変わっていた。コピーよりも,極力推測入力を使って入力するのが無難だと思う。

import Cocoa

class MyTableView: NSTableView, NSTableViewDataSource, NSTableViewDelegate {

// (your code)

}

という感じである。

NSTableViewDataSource protocol

「Cell-based」「View-based」共通の様。最低限2つのメソッドを実装する必要がある。表の再表示が必要な度に呼び出される。頻繁にTableViewが確認してくるわけだ。

optional func numberOfRowsInTableView(_ aTableView: NSTableView) -> Int

 見た通り,何行ありますか?と確認するメソッドである。列を聞くメソッドはない。列の数が固定であれば,最初に.xibファイルの段階できちんと列を作っておけばよい。可変の表であれば,列の数が変わる度に自分でNSTableColumnのインスタンスをprogrammaticalに加えたり減らしたりしなければならない。このように列と行で扱いが全く異なる。

optional func tableView(_ aTableView: NSTableView, objectValueForTableColumn aTableColumn: NSTableColumn?, row rowIndex: Int) -> AnyObject?

どのTableColumnの何行目は何の値を表示させますか?と確認するもの。SwiftになってIntのような整数であっても何でも同じように返せるようになった。戻り値がoptional valueなのでnilを返すこともできる。

以上の2つはほぼ「絶対」実装しなければならないものである。

optional func tableView(_ aTableView: NSTableView, setObjectValue anObject: AnyObject?, forTableColumn aTableColumn: NSTableColumn?, row rowIndex: Int)

「Cell-based」の時のみ,ユーザが入力した値をどう処理するかを書くメソッド。入力のまま採用するのか,自動的に修正を加えるのか,またはその入力を破棄するのかがここで制御できる。何の値を表示させるかに反映するようにデータを変えればよい。つまり,何もしないでreturnさせれば,その入力は無視され,また元の値が表示される。

そもそもユーザがダブルクリックして一時的にでも値を書き換えるのを許可するのかどうかはdataSource protocolではなくdelegate protocolのメソッドで制御する(後述)。

セルをNSTextFieldCellではなくNSButtonCellのようなボタンにしている場合,delegate protocolの値書き換え制御のメソッドの設定や同じくdelegate protocolのセルの属性の設定(セルがenableかどうか)に関わらず,ユーザがボタンを押せばこのメソッドが呼び出される。どのような場合でも押した後の挙動をここで定義しておく必要がある。

NSTableViewDelegate protocol

表の挙動などを制御する。ここに定義されているメソッドで足りるのであれば,敢えてNSTableViewのサブクラスを作る必要はない。仮にそのサブクラスを作った場合でも,このプロトコールを使った方が簡単にかけることも多いし,サブクラスでは対応できない挙動も定義することができる(と思う)。

ここは「Cell-based」か「View-based」の表かで実装するものがかなり異なる。しかも,現時点のApple提供のDocumentation and API Referenceではどちらで使うメソッドか明示されていないことが多い(よく読めば分かるが)。注意が必要だ。

私が重宝した代表的なメソッドを挙げる

optional func tableView(_ aTableView: NSTableView, shouldEditTableColumn aTableColumn: NSTableColumn?, row rowIndex: Int) -> Bool

ユーザがダブルクリックしたときに書き換えられるようにセルの中に入れてあげるかどうかに答えてあげるメソッド。先程も解説したようにボタンの場合はこのメソッドの内容に関係なく押されてしまう。

NSTableViewのサブクラスを作って func mouseUp(_ theEvent: NSEvent) を上書きしたりする(NSTableViewのひいおじいさんがNSResponderにあたるのでこのメソッドを上書きすることができる)とこのdelegateメソッドは呼び出されなくなる。このメソッドとは別にfunc mouseUp(_ theEvent: NSEvent) で制御した上で editColumn(_ columnIndex: Int, row rowIndex: Int, withEvent theEvent: NSEvent?, select flag: Bool) を呼び出すことと入力を許可することができる。このメソッドの解説の中で「you should rarely need to invoke it directly」となっているので一抹の不安があったが,それを使って特に不具合はなかった。ここまで書くのだからこのメソッドを使わずにdelegateメソッドを呼び出す方法があるのかもしれないがよくわからない。

optional func tableView(_ aTableView: NSTableView, willDisplayCell aCell: AnyObject, forTableColumn aTableColumn: NSTableColumn?, row rowIndex: Int)

「Cell-based」の時のみ使うメソッドだと思う。この構文で分かるとおり。セルを自分で入れ替えたりはできない。あくまでそのセルの属性を変更するだけだ。表を再描画させる度,再描画させるセルの分だけ呼び出される。このメソッドを使って特定の行・列のセルの色を変えたりしてセルの選択を表現するようにした。

optional func tableView(_ tableView: NSTableView, didClickTableColumn tableColumn: NSTableColumn)

表のヘッダーのクリックはNSTableViewのサブクラスの func mouseUp(_ theEvent: NSEvent) では制御できなかったのでここで制御した。その他ヘッダーを使って操作するもの(tableColumnをresizeしたりreorderしたり)は軒並みそうなので,delegate protocolを使う。

NSTableViewのサブクラス

ユーザが指定したセルを起点にcopyやimportした表を挿入するためには,ユーザがクリックしたセルをマークしなければならない。delegate protocolだけでは実現できないと結論し,4.0ではサブクラスを作った。そうなると普通の表計算ソフトにあるような複数のセルをドラッグで選んだりする動作もどのように実現できるのか試してみたくなった。ScatterMakerには必要のない機能であるが,実装に挑戦してみた。そのため,NSTableViewのひいおじいさんのNSResponderの3つのメソッド

func mouseDown(_ theEvent: NSEvent)

func mouseDragged(_ theEvent: NSEvent)

func mouseUp(_ theEvent: NSEvent)

を上書きした。なにしろひいおじいさんのメソッドなので,theEventの内容を取り出してどのセルに対する操作かを判別しなければならない。位置のcoordinateをwindow内のものからview内のcoordinateに変換し,さらにそれが何列目で何行目のセルなのかを割り出していく。ダブルクリックなのかシングルなのかは func mouseUp(_ theEvent: NSEvent) の中でクリックの回数で独自に判別しなければならない。optional func tableView(_ aTableView: NSTableView, shouldEditTableColumn aTableColumn: NSTableColumn?, row rowIndex: Int) -> Boolが呼び出されなくなるので書き換えをさせるかどうかはここで判断させなければならない。

やはりサブクラスはどうしても必要がないのであれば作らない方がよい,と思う。

macOSアプリケーションのプログラミング - アウトライン表

ScatterMaker4でも取りあえず「Cell-based」で作ってみたものの,分かりやすいインターフェイスにするためには非常に問題がある。やはり,一つの列にいくつかの種類のCellを使いたい。また,Appleからのドキュメントをみても「Cell-based」は「legacy」なであり,新しいプロジェクトには非推奨,となっている。時期バージョンでの切替を目指し,outlineViewから「View-based」に切り替えていくことにした。実装の仕方の違い,挙動の違いでかなり戸惑ったので,備忘録の意味もこめてまとめてみる。

NSOutlineViewはNSTableViewのサブクラスであるし,戸惑ったポイントはNSTableViewと共通の問題と思われたので,メインの部分はtableViewのところにまとめた。

2015年6月

NSOutlineViewDataSource protocol

ここはview-basedでも大きく変わらない。

optional func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject

optional func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool

optional func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int

optional func outlineView(_ outlineView: NSOutlineView, objectValueForTableColumn tableColumn: NSTableColumn?, byItem item: AnyObject?) -> AnyObject?

を実装する所まではcell-basedと同じで,値を表示させる「....... objectValueForTableColumn ......」はあるのに

optional func outlineView(_ outlineView: NSOutlineView, setObjectValue object: AnyObject?, forTableColumn tableColumn: NSTableColumn?, byItem item: AnyObject?)

に対応するメソッドがなく,前者の扱いが中途半端で不思議な感じもtableViewと共通である。詳しくは表 TableViewのページで。

NSOutlineViewDelegate Protocol

最低でも以下のメソッドは実装しなければならない。

optional func outlineView(_ outlineView: NSOutlineView, viewForTableColumn tableColumn: NSTableColumn?, item item: AnyObject) -> NSView?

概ねtableViewと同じなのでこの習性もtableViewのところに記載した。

2014年12月

基本

NSOutlineViewはNSTableViewの子どもであるので,似通っている部分も多い。TableColumnに関する考え方や扱いは同様である。ただし,行に関する考え方は大きく異なっている。TableViewの時はただ「何行目か」というだけで済んだが,outlineViewの場合は,そこに従属するデータがあるのかないのか,行ごとの特性が大きく異なる場合も多く,行ごとの管理も必要となるので当然であろう。それを管理するのに下のメソッドの紹介でも分かるとおり,"item"という概念を導入している。DataSource protocolやDelegate protocolで制御するという概念は全くtableViewと同じであるがこの"item"の概念を理解するのが第一歩である。

通常新しいクラスを作ってメソッドの上書きをしなければならない時やdataSourceやdelegate protocolのメソッドを書かなければならない時などの,引数や戻り値は何らかの実体があるものが多いと思う。例えば表示させる値であったり,何かの数であったり,パーツの一部であったり,というように。しかし,この"item"はコード作成前の時点では実体のない概念である。むしろ,この概念にあうようにその"item"をデザインして作成することが手っ取り早いように思う。つまり大本の"item"があってその中にさらに"item"があって,というような構造を自分で作っていくのがよいようだ。

元のデータがdictionaryやarrayで構成されている場合,さらにそのデータと"item"に対応させた自前のオブジェクトをシンクロナイズさせるメソッドを作らなければならない。それはそれで面倒であるが,下のdataSourceやdelegate protocol以外でもいろんな制御があるので,無理やり普通のdictionaryやarrayを"item"の概念に当てはめて下のdataSource protocolのメソッドを書くよりは"item"に対応させた自前のクラスを作った方がよいように思われる。

私の場合,このようなオブジェクトをこの"item"のために作った。

class MyOutlineItem: NSObject {

var column0 = ""

var column1 = ""

var children: [MyOutlineItem] = []

let parentItem: MyOutlineItem? // 大本の"item"に親はいないので,optional valueにしておく

init(column0: String, parentItem: MyOutlineItem?) {

// (your code)

}

// (your code)

}

という感じである。列の数が固定でなければ別の書き方があると思うが,固定の場合は単純にこのような構造を持たせておけばこれだけでも十分に下のprotocolに対応できると思う。

NSOutlineViewDataSource protocol

最低でも実装しなければならないメソッドは下の最初の4つである。NSTableViewの2つに比べると多いが,最初の3つは"item"に対応させたクラスを作っておけば下のサンプルコードそのままで問題ない。

下の一つ目と三つ目のメソッドの一行目が特徴的である。NSOutlineViewの第一階層の"item"には親はいないのでそこは"nil"と定義されている。下のサンプルコードでは,dataSource側に用意したデータの構造内の,OutlineViewの第一階層の親に対応するオブジェクトは"rootItem"である。この概念が理解できればNSOutlineViewはほぼ使いこなせる,と言ってよいだろう。

optional func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject

あなたの何番目のこどもは何ですか?を返すメソッド。先述のように,outlineView上では第一階層の親はいない。なので第一階層に関しては,「OutlineView上では親は居ない場合」の定義をしなければならない。データ構造上は第一層のデータを束ねる"rootItem"を下の場合は定義しているので,それを返せば良い。

func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject {

if item == nil { return rootItem.children[index] }

return (item as MyOutlineItem).children[index]

}

optional func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool

子どもがいるかどうかを訪ねるメソッド。これは見たまま。

func outlineView(outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool {

return (item as MyOutlineItem).children.count > 0

}

optional func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int

子どもは何人いるのかを尋ねるメソッド。1番目と同様。第一階層の答え方だけ注意。

func outlineView(outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int {

if item == nil { return rootItem.children.count }

return (item as MyOutlineItem).children.count

}

optional func outlineView(_ outlineView: NSOutlineView, objectValueForTableColumn tableColumn: NSTableColumn?, byItem item: AnyObject?) -> AnyObject?

上の3つがちゃんと書ければ,これはNSTableViewの時と考え方は変わらない。この"item"ではこの列の場合,何の値を表示させますか?と聞くメソッド。OutlineViewの場合は何行目,という定義が難しいので,"item"が重要な概念だ。

optional func outlineView(_ outlineView: NSOutlineView, setObjectValue object: AnyObject?, forTableColumn tableColumn: NSTableColumn?, byItem item: AnyObject?)

これも"item"の概念をクリアできていれば,NSTableViewと同様。ボタンを配置した場合の挙動も同じで,下のdelegate protocolのメソッドの設定に関わりなく,ボタンは押される。

NSOutlineViewDelegate protocol

optional func outlineView(_ outlineView: NSOutlineView, shouldEditTableColumn tableColumn: NSTableColumn?, item item: AnyObject) -> Bool

NSTableViewと同様,ダブルクリックしたときにセル内に入ることを許可するかどうかを尋ねるメソッド。今回凝ったoutlineViewを作る予定はなかったので,NSOutlineViewのサブクラスは作らなかった。なのでここでそれを制御。

optional func outlineView(_ outlineView: NSOutlineView, willDisplayCell cell: AnyObject, forTableColumn tableColumn: NSTableColumn?, item item: AnyObject)

NSTableViewのメソッドと名前が似ているものは大体意味も同じ。「Cell-based」のoutlineViewの時に使うメソッド。やはり,セルを入れ替えたりはできないのでいろんなセルをどんどん使うことはできない。同じ列(tableColumn)内ではセルの種類は統一する必要がある。それ以上に凝ったoutlineViewを作るには「view-based」で作るしかない。今回view-basedで作れば,「Main」のoutlineViewは1列減らすことができたと思われる。今回は時間的問題でview-basedを勉強することを回避した。今後は検討である。

macOSアプリケーションのプログラミング - 文字の表示

2014年12月

基本

文字の表示のさせ方はいくつかある。ほんのちょっと書くときとかそのオブジェクトを選択したりする必要がないときなどは,view上に直接書けば良い。ScatterMaker2.0まではそのようにしていた。しかし,少し進んだ制御をする場合は,何らかのサブクラスをつくって表示させるのがよいようだ。ScatterMakerにおいては,ドット使う場合やx軸のタイトルなど,同じテキストを表示させることも多い。view上でも直接編集することができるようにするのも,自然な動作を実現するには必要だ。これを実現するために適切な方法かはわからないが,NSTextViewをサブクラス化して使ってみることにした。

NSTextViewもデータを直接保持はしておらず,表示・編集の場である。データはNSTextStorageのインスタンスが持つことになる。一つのNSTextStorageに複数のNSTextViewがぶら下がる。同じものを異なる場所に表示させていくので,NSLayoutManagerまではNSTextViewと1対1に対応させた。

最低限下のようにセットしていくことが必要である。NSTextStrorageにNSLayoutManagerをaddし,NSLayoutManagerにNSTextContainerをaddする。注意が必要なのはNSTextViewとNSTextContainerの関係だ。そこはaddではないので,NSTextViewを保持しただけでは保持されない。よって正しく表示させるためにはNSTextStorageも別にしっかり保持する必要がある。

var aTextStorage = NSTextStorage(string: str, attributes: usableAttr)

var layoutManager = NSLayoutManager()

var textContainer = NSTextContainer(containerSize: newRect.size)

layoutManager.addTextContainer(textContainer)

aTextStorage.addLayoutManager(layoutManager)

var aTextView = MyTextView(frame: newRect, textContainer: textContainer)

macOSアプリケーションのプログラミング - フォントの変更

2014年12月

基本

これもなかなかAppleの「Documentation and API Reference」だけを読んでも実装が難しかった分野。

"The font manager responds to a font-changing action method by sending a changeFont: action message up the responder chain."

という文がある。つまりはNSResponderの子孫であればresponder chainに投げられたメッセージを受け取ることができるので,下のメソッドを実装すれば受け取ることができる。フォントの種類や大きさを変更すると下の一つ目のメッセージが投げられるようだ。文字の色を変更した場合はなぜか,一つ目の"changeFont:"の次に"changeAttributes:"が投げられるようだ。その挙動を理解して実装しなければならない。何でこのような挙動になっているのか,このような挙動で何のメリットがあるのか私自身は理解していない。

func changeFont(_ sender: AnyObject?)

下がサンプルコードである。NSWindowControllerのサブクラスで受け取るとなぜか"override func"となった。その下の"changeAttributes:"は異なる。理由は私には分からない。

上のAPI Referenceで分かるとおり,このメッセージを投げた"sender"は"font manager"であるから,"let newFont = sender!.convertFont(oldFont)"というメソッドが使える。"newFont"がユーザが"FontPanel"で選んだ新しいフォントである。プログラム上はこの"oldFont"は何でも動作するのであるが,願わくば,"oldFont"を元々使用していたフォントと同一となるようにしておくとユーザの混乱がないと思われる。フォントを設定する可能性のあるものを選んだりしたときなど,まめにシンクロさせておくとよいと思う。

override func changeFont(sender: AnyObject?) {

// (your code)

let oldFont = (your old font as NSFont)

let newFont = sender!.convertFont(oldFont)

// (your code)

}

func changeAttributes(_ sender: AnyObject?)

投げられる順番の挙動を理解すれば,後はFontの時と同様。

func changeAttributes(sender: AnyObject?) {

// (your code)

let oldAttr = (your old attributes as [String: AnyObject])

let newAttr = sender!.convertAttributes(oldAttr)

// (your code)

}

macOSアプリケーションのプログラミング - プリント

2014年12月

ScatterMakerから直接プリントして使う用途がそうそう存在するとは思えないが,標準でついている機能を消すのも何なので実装に挑戦した。Appleからの「Documentation and API Reference」を探して何とか"Print..."に対応することはできたが,用紙設定の情報をDocumentに反映させるために情報を拾うことがなかなかできない。メソッドを一つ一つ調べて何とか実装することができた。もっとスマートな広い方があるのか分からないが下のメソッドを使った。実装の仕方を調べるのに苦労したが,「Document-based」のアプリケーションであれば,2つのメソッドを上書きすれば最低限,用紙設定と印刷に対応できるので,分かれば簡単,と言える。

NSDocument

「Document-based」であれば,これのサブクラスをつくってDocumentを作っているはず。そのサブクラスで下の2つのメソッドを上書きする。

func shouldChangePrintInfo(_ newPrintInfo: NSPrintInfo) -> Bool

このメソッド内で用紙設定の情報を拾うことにした。trueを返せば新しい設定が書類の共通のプリント設定(self.printInfo)として保存される。trueを返す前に"newPrintInfo"から大きさの情報を取り出して,必要なviewに設定する。

func printOperationWithSettings(_ printSettings: [NSObject : AnyObject], error outError: NSErrorPointer) -> NSPrintOperation?

印刷したいviewを選んで下のようにNSPrintOperationのインスタンスを返すとそれが印刷される。

NSPrintOperation
init(view aView: NSView, printInfo aPrintInfo: NSPrintInfo) -> NSPrintOperation

return NSPrintOperation(view: (プリント対象のview) printInfo: self.printInfo)

できてしまうとびっくりするほど簡単である。