2010-12-08

アプリを開くリンク #1 - 内部リンクの置き換えのために (MacBloggerGlass)

以前の記事で、内部リンクの置き換えを実現するためには記事内容をファイルとしてローカルのストレージに書き出すしかないかも、と書いた(→「記事の表示機能を作る (MacBloggerGlass)」)。ここ数日、Cocoa Bindings の使い方をあれこれ調べる間もそのことについて考え続けてきた。今日になって、「あれ、そう言えば iTunes Store のリンクって Safari から iTunes に飛ぶよな」と気付いた。

たとえば、以下のリンクは「itms://」で始まる URL で、Safari でクリックすれば iTunes.app で Coldplay の新曲(2010-12-08 時点)のページが開く(Mac と iPad で確認)。

itms://itunes.apple.com/jp/album/christmas-lights-single/id406970808

ちなみに、Safari 以外のブラウザだと、iTunes.app を開く前に確認のダイアログが現れる。右のスクリーンショットは Google Chrome でこのリンクをクリックしたときに出てくるものだ。

Safari (つまり WebView) でリンクをクリックしてアプリに通知させることができるなら、内部リンクをそれで置き換えれば良い。そうすれば MacBloggerGlass (以下、MBG) から、MBG 自身に開きたい記事の情報を伝えることができる。フィードデータから記事ごとに HTML ファイルを作って書き出して、file:/// でアクセスするよりもスマートだ。

もっとも、MBG を作る動機にもなっている「オフラインでも読める」ようにするためには、記事の内容をふくめたフィードデータをローカルに保存しなくてはならないんだけど。

ともあれ、これで方向性が定まった。まずは「アプリを開くリンク」について調べるところからだ。

itms:// を開こうとしたとき、誰が iTunes を起動するのか?

では、特定の URI スキーム (http://... の http の部分) とそれを扱うアプリの組み合わせを知っているのは誰だろう? Safari のようなブラウザ自身だろうか? 少なくとも Mac OS X では(おそらく iOS でも) 専用のサービス (API) が用意されている。

(「Launch Services Programming Guide: Introduction」より)
Launch Services is an API that enables a running application to open other applications or their document files or URLs (uniform resource locators) in a way similar to the Finder or the Dock. Using Launch Services, an application can perform such tasks as:

  • Open (launch or activate) another application
  • Open a document or a URL in another application
  • Identify the preferred application for opening a given document or URL
  • [...snip...]

この Launch Services がドキュメントのタイプや URL タイプとアプリの組み合わせを保持している。では、Launch Services はその組み合わせの情報をどこから得るのか? それはアプリが持っている Info.plist の記述からだ。

具体的には、Info.plist 中の CFBundleURLTypes にアプリが開くことのできる URL タイプが記述されている。

Property List の書き方

Info.plist は Property List と呼ばれる XML 形式で記述する。以下に Xcode が作るデフォルトの Info.plist を示す。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
 <key>CFBundleDevelopmentRegion</key>
 <string>English</string>
 <key>CFBundleExecutable</key>
 <string>${EXECUTABLE_NAME}</string>
 <key>CFBundleIconFile</key>
 <string></string>
 <key>CFBundleIdentifier</key>
 <string>com.yourcompany.${PRODUCT_NAME:rfc1034identifier}</string>
 <key>CFBundleInfoDictionaryVersion</key>
 <string>6.0</string>
 <key>CFBundleName</key>
 <string>${PRODUCT_NAME}</string>
 <key>CFBundlePackageType</key>
 <string>APPL</string>
 <key>CFBundleShortVersionString</key>
 <string>1.0</string>
 <key>CFBundleSignature</key>
 <string>????</string>
 <key>CFBundleVersion</key>
 <string>1</string>
 <key>LSMinimumSystemVersion</key>
 <string>${MACOSX_DEPLOYMENT_TARGET}</string>
 <key>NSMainNibFile</key>
 <string>MainMenu</string>
 <key>NSPrincipalClass</key>
 <string>NSApplication</string>
</dict>
</plist>

URL タイプは、トップレベルにある dict 要素の子として記述する。つまり、CFBundleExecutable などと同じレベルの要素にする。その際、CFBundleURLTypes が dict の array になるように構成する。そして、各 dict の中身には以下の 4 つの要素を置く。

CFBundleURLType 内の要素 (「Launch Services Programming Guide」より)
Key Type Description
CFBundleTypeRole string アプリがドキュメントをどのように扱うものなのか。Launch Services が認識するのは Editor、Viewer、そして None の 3 つのみ。
CFBundleURLName string URL タイプの名前
CFBundleURLIconFile string このタイプの URL を表示する際に使われるアイコンファイルの名前
CFBundleURLSchemes array このタイプにふくまれる URL のスキームの配列。各スキームは string として記述する。

典型的な CFBundleURLTypes の構造は以下のようになる。

CFBundleURLTypes
+-- array
    +-- dict
    |   +-- CFBundleTypeRole
    |   |   +-- string
    |   +-- CFBundleURLName
    |   |   +-- string
    |   +-- CFBundleURLIconFile
    |   |   +-- string
    |   +-- CFBundleURLSchemes
    |       +-- array
    |           +-- string
    |           +-- string
    |           ...
    +-- dict
        ...

iTunes.app の Info.plist を覗いてみると、CFBundleURLIconFile は省略しても構わないようだ。

以下のスクリーンショットは、実際に Xcode 中で Info.plist を編集し、URL タイプを 1 つ、追加したところのものだ。

Info.plist に URL タイプを追加する様子
左に表示されている要素の種類はドロップダウンリストの中から選ぶことができる。トップレベルで URL Types を選ぶとその子要素の構造も同時に作られる(デフォルトでは URL Identifier だけが追加されている)。この例では URI スキームとして sample という文字列を指定している。

アプリ側での作り込み

Launch Services はアプリに対してアプリの起動や URL を開く等の Apple イベントを送ってくる。つまり、アプリは送られてきたイベントを適切に処理することができなければならない。

そのためにはどういうコードを書けば良いのか? それは次回の記事で(ドキュメントは見つけてあるがまだ読んでいない→「Cocoa Scripting Guide: How Cocoa Applications Handle Apple Events」)。

関連リンク

関連記事

twitter より (2010-12-07)

Powered by twtr2src.

2010-12-07

Cocoa Bindings の肝は KVC 準拠だ

ようやく Cocoa Bindings の謎が解けたように思う。少なくとも、Cocoa Bindings を使ったアプリを作るための「心得」のようなものを見つけることができた。以下で、それを簡単に説明してみる。

用語の整理

まずは用語の整理。Cocoa Bindings 関連のドキュメントを読むためには以下の KVC の用語とそれが意味する概念を理解しておく必要がある。

KVC (Key-Value Coding) とは、オブジェクトが持つプロパティに対してキーとなる名前を介してアクセスすること、およびそのためのオブジェクト側の実装のことだ。「KVC でアクセスする」とか「このクラスは KVC に準拠させてある」などと使う。

プロパティ(property)とは、オブジェクトがその内部に抱える「何か」のことで、典型的にはオブジェクトの内部状態を示す様々な値のことだ。ただし、プロパティとはオブジェクトの外部から見えるもののことで、必ずしもプロパティの値そのものが内部で保持されているとは限らない。プロパティとはオブジェクトのインタフェースのうち「何か」を返すものと言っても良い。

プロパティはその値とオブジェクトとの関係によって、以下の 3 つに分けられる。

  • 属性(attribute)
  • 対一関係(to-one relationship)
  • 対多関係(to-many relationship)

属性(attribute)とはプロパティのうち、Objective-C で言う単純型のことで、数値や文字列の他、NSColor や NSNumber のような不変オブジェクトもふくまれる。

対一関係(to-one relationship)とは、プロパティがそれ自身のプロパティを持ったオブジェクトの場合を言う。以下に示す、AppController のプロパティ entry がこれにあたる。

@interface Entry : NSObject {
    NSString *title;
    NSString *content;
    NSDate *lastUpdateDate;
}

@interface AppController : NSObject {
    Entry *entry;
}

対多関係(to-many relationship)とは、簡単に言えばプロパティが配列のような集合型になっている場合のことだ。以下の Feed のプロパティ entries がこれにあたる。

@interface Feed : NSObject {
    NSMutableArray *entries;
}

KVC 準拠

あるクラスを(正確にはあるクラスの特定のプロパティを) KVC 準拠にするためには、いくつかの要件を満たさなければならない。これは、要は、プロパティに対するアクセス用メソッドを一定の規則による名前で作る、ということだ。

たとえば、title という文字列のプロパティを持ったクラスの場合なら、以下の 2 つのメソッドを定義する。

- (NSString *)title;
- (void)setTitle:(NSString *)aTitle;

この 2 つがあるなら、必ずしも属性を保持するインスタンス変数を定義する必要はない。

加えて、プロパティへのアクセスにはすべて(クラスの内部でも) KVC 方式を用いる。これは準拠に必須ではないが、こうすることで KVO が正しく機能する。KVC 方式とはインスタンス名に「.」(dot) でプロパティ名を連結した記法のことだ。たとえば、先のクラスのインスタンス名が entry なら、entry.title としてアクセスする。クラス内部では、インスタンス名として self を使えば良い。つまり、self.title で読み書きする。

対一関係の場合も同様だ。さらに、この場合、プロパティも KVC に準拠しているなら「.」でつなげてアクセスする。たとえば、self.entry.title のように。

対多関係の場合の追加

一方、対多関係の場合はプロパティが配列(の類)であるため、その要素にアクセスするためのメソッドも必要になる。最初に挙げた Feed の場合、以下のようなメソッドを定義する必要がある(要素として Entry を抱えると想定)。

// for KVC compliance (required)
- (void)insertObject:(Entry *)entry inEntriesAtIndex:(NSUInteger)index;
- (void)removeObjectFromEntriesAtIndex:(NSUInteger)index;
- (void)replaceObjectInEntriesAtIndex:(NSUInteger)index withObject:(Entry *)entry;
// for KVC compliance (optional)
- (int)countOfEntries;
- (Entry *)objectInEntriesAtIndex:(NSUInteger)index;

これらのメソッド名にはプロパティ名が埋め込まれている。たとえば、insert の場合、insertObject:in<key>AtIndex: の key のところにプロパティ名の entries が埋め込んである。他も同様だ。

KVO 準拠

KVO 準拠で何より大事なことはプロパティのアクセスはすべて KVC 方式で行う、ということだ。たとえば以下は、ここ数日、悩まされてきたエラーメッセージだ。このエラーの原因が、あるクラスで自分自身のプロパティへのアクセスに KVC 方式を使っていなかった(部分があった)ことにあった。

(実行時のログより)
Cannot update for observer <NSAutounbinderObservance 0x113761290> for the key path "feed.entries" from <PreferenceController 0x11375cc50>, most likely because the value for the key "feed" has changed without an appropriate KVO notification being sent. Check the KVO-compliance of the PreferenceController class.

具体的には、以下のようなコードになっていた。本来、reload の中で newArray をプロパティにセットした際に、これを監視しているオブジェクトたちに通知が飛ぶことになる。ところが、reload が実行される前に awakeFromNib の中で KVC 方式を使わずに entries を変更してしまっていたため、この通知が送れなくなっていた。それが上のエラーだ。awakeFromNib の中でも、reload と同様に self.entries という表記を使うようにすることで、この問題は解消した。

@implementation AppController
[...snip...]
- (void)awakeFromNib
{
    [...snip...]
    entries = [[NSMutableArray alloc] init];
    [...snip...]
}

- (IBAction)reload:(id)sender
{
    [...snip...]
    self.entries = newArray;
    [...snip...]
}

また、対多関係プロパティの場合、プロパティへの要素の追加・変更・削除では、先述のように KVC 準拠のために定義したメソッドを使わなければならない。

まとめ

以上が、ここ数日、悩まされてきた問題に対する一応の解答になる。まとめると、Cocoa Bindings を使ったアプリを作る上で肝心なことは、とにもかくにも KVC 準拠ということになる。

そして、KVC 準拠でクラスを作ろうと思うなら徹底して KVC 準拠にすることだ。特定のプロパティだけを KVC 準拠にしようなんて中途半端なことを考えていると、抜けが出てくる。ひとつのクラスの一部は KVC 準拠で他は違うというような状態では、(プログラムが動かないとわかってから)準拠の抜けを探すのは難しい。デバッグで苦労するよりも、コーディング中に少し余分に気を使う方が良い。

Cocoa Bidings を使ったアプリ (MVC アーキテクチャを想定) を作るなら、まずはモデルのクラスをを KVC 準拠で作ること。また、モデル内部の他のメソッドでも、モデルオブジェクトを扱うコントローラの実装でも、とにかくモデルのプロパティへのアクセスには KVC を使うこと。対多関係を使うなら(モデルが配列等を内部に抱えるなら)、内部の配列に直接アクセスするのではなく、insert や remove、replace といったメソッドを KVC 準拠の形式で定義すること。

あとひとつ付け加えるとすれば、やはり Interface Builder に対する慣れだろうか。インスペクタで GUI 部品の設定をあちこちいじっていると、どこをどう変更したのかがわからなくなってくる。どうしても動かなくて困っていたところ、部品を一度削除して最初から設定し直すと動いた、なんてことが時々起こる。こればかりは慣れるしかないと思う。部品を汎用に作ろうとすれば設定項目を増やさざるを得ず、設定項目が増えればそのためのツールは複雑になるものだから。

参考文献

詳解 Objective-C 2.0
荻原 剛志
ソフトバンククリエイティブ ( 2008-05-28 )
ISBN: 9784797346800

関連リンク

関連記事

2010-12-06

まだ Cocoa Bindings の謎が解けない

前回の状態(→「Cocoa Bindings がわからなくなってきた」)からまだ抜け出せない。以下に、今、ハマっている状況を簡単に説明してみる。

モデルとコントローラの構造と関係は以下のようになっている。Feed の entries は配列(NSMutableArray)で Entry のインスタンスを抱えている。Entry のプロパティは文字列(NSString)や日付(NSDate)といったオブジェクトだ。このモデルをアプリの主コントローラが抱えている、という形だ。Atom 形式のデータの構造を扱うアプリを想定している(MacBloggerGlass もその一つ)。

+--------------+
| AppContoller |
+--------------+        +--------------+
| feed        --------->| Feed         |
| ...          |1     1 +--------------+       +--------------+
+--------------+        | entries     -------->| Entry        |
                        | ...          |1    * +--------------+
                        +--------------+       | title        |
                                               | content      |
                                               | lastUpdate   |
                                               | ...          |
                                               +--------------+

やりたいことは、Feed が抱える Entry オブジェクトのデータを表(NSTableView)に写し出すことだ。

Cocoa Bindings を使わないなら、AppController を NSTableView のデータソースにすることになる。これは簡単に言うと、NSTableView から「この行のここの列に表示するデータはどれ?」という問い合わせに AppController が答える方式だ。行と列を指定して、配列に収められたオブジェクトのプロパティを取り出すコードを AppController に実装することになる。

Cocoa Bindings を使えばそのコードを書かなくて済む。NSArrayController を介してモデルとビュー(NSTableView)を結びつけることでコードを書かずに同じことを実現できる……はずなのだが、なんだかうまくいかない。いや、うまく動くときもある。まったく動かないなら間違っていることだけは確実にわかる。けれど、同じように書いたり設定したりしたつもりのものが動いたり動かなかったりすると、かえってわかりにくくなる。

どうやら、肝となるのはモデルとコントローラが KVC と KVO に準拠することらしい。で、KVC に準拠するにはどうすれば良いか、についてはわかってきた。だが、KVO への準拠のための施策が良くわからない。ドキュメント(Key-Value Observing Programming Guide)に Ensuring KVO Compliance という項目があるものの、そこに書かれているのは 2 つの項目だけ。しかもそのうちの 1 つは KVC に準拠しろ、というもの。残る一つがこれ。

(「Key-Value Observing Programming Guide: Ensuring KVO Compliance」より)

  • The class must allow automatic observer notifications for the property, or implement manual key-value observing for the property.

これだけじゃなあ。もう少し細かく書いて欲しいよ。(´・ω・`)

とにかく、今日も先には進めていない。明日も、今日の続きで Cocoa Bindings の謎と格闘することになる。

追記@2010-12-08

この記事に書いた問題はひとまず解決した。「Cocoa Bindings の肝は KVC 準拠だ」を参照のこと。

関連リンク

関連記事

2010-12-05

Cocoa Bindings がわからなくなってきた

この数日、Cocoa Bindings の使い方で悩んでいる。というより、混乱している。

MacBloggerGlass に環境設定パネルを作る際にモデルとビュー(パネルに配置した GUI 部品)を結びつけるために、Cocoa Bindings を使った。試行錯誤とドキュメントの流し読みで、どうにか動くものができた。その過程で Cocoa Bindings についても理解した、と思っていた。

同じことをアプリのメインウィンドウでもやろうとした。環境設定パネルでブログの一覧をテーブルビューに表示させたように、メインウィンドウでも記事一覧をテーブルビューに表示させるのに Cocoa Bindings を使おうとしたのだ。これがうまくいかない。環境設定パネルの時と同じように書いたら動かない。

試行錯誤を繰り返し、メインウィンドウでも表示できるようになったが、そのコードは環境設定パネルのものとは少し異なっていた。具体的には、プロパティの更新を KVO で通知する部分が違う。

なにかおかしい。もちろん、おかしいのはわたしの理解の方だ。基本に立ち返って、KVC、KVO の理解を点検するところから始めようと思う。ここをクリアにしない限り、先には進めない。

追記@2010-12-08

この問題については、以下の後続記事を参照のこと。一応、解決している。

関連リンク

関連記事