Xcode Plugin が盛り上がっているらしい

しばらく前に簡単なプレゼン資料を作って一部の人にブログに書く書く詐欺をしていたので、以下のクラスメソッドさんの記事に便乗して書いておこうと思います。

基本的には重複してるし自分のエントリの方が適当なのでクラスメソッドさんの記事を参照してください。

Xcode Plugin とは

  • Xcode は元々それ自身が複数の Plugin の塊
    • Xcode 5.0.1 内部には 54 個の Plugin がある
  • 3rd Perty が勝手にその Plugin の仕組みを使って自身のコードを Xcode にロードさせることで自分が欲しい機能を追加している。
  • 公式な方法ではないため API はない。
  • おのおのが探しだして追加する

ロードの仕組み

Info.plist の "Principal class" に読み込んでもらいたいクラス名を記載します。ここがエントリポイントとなります。Principal class となるクラスの + pluginDidLoad:(NSBundle *)plugin メソッドを実装します。ここでインスタンスを生成してゴニョゴニョという感じです。  

とかは面倒くさいので以下の便利なテンプレを組み込んでおいたら新規プロジェクトを作成するときに Xcode Plugin のひな形ができるでそっちの方が楽だと思います。これでビルドするだけで Menu の中に独自定義のメニューが足されます。

どこに何をどうやって足す

  • class-dump
  • otool

とかを使って Xcode にあるクラスやメソッドをごそっと書き出して、それらしいクラスやメソッドを探す。 試します。このあたりは Cocoa 全般の知識やプラグイン作成経験が生きてくる作業です。

自分で class-dump するのも面倒くさいのでここを参照

あとは既に Xcode Plugin が多数出ているしそれらのソースコードはたいてい Github に上がってるから自分がやりたいことと似ていることをやっているのを探してコード読むって感じです。

今回作ったやつ

  • Xcode のコンソールに出力されているテキストを
    • プロジェクト名+日時 なファイルとして書き出す
    • それだけ。

ハマりどころや Tips

iOS 開発者視点で。

Xcode はマルチウィンドウ

プラグインのロードは1回だけどウインドウは複数ある

  • ウインドウに足した UI のインスタンスをプロパティとかでそのまま持つとおかしくなる
  • ウインドウごとにプラグイン側で独自に管理するか都度取得で対処。
  • 今回はウインドウがアクティブになったタイミングで必要なインスタンスを取得。
  • Xcode 側に足した UI は tag で管理して,存在しなければ追加する感じで対処。

UserDefaults は親の物

ちょっとした情報を保存したいときの典型

[[NSUserDefaults standardUserDefaluts] setObject:設定情報 forKey:なんか設定];

これをやるとどこに保存されると思いますか?

これは Xcode の Preference に保存されます。

~/Library/Preferences/com.apple.dt.Xcode.plist
  • 勝手プラグインが親の Preference 汚すってダメだろ
  • アンインストールしても com.apple.dt.Xcode.plist に残ってる

ほとんどの Xcode Plugin は普通に Defaults に書いてしまってるんですが、行儀が悪いので自分のプラグイン用の plist を用意してそこに設定情報を書き込みましょう。

[NSBundle bundleForClass:[self class]];

これで自分のBundleのインスタンスがとれるので

NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
NSString *path = nil;
if ([paths count]) {
    NSString *lastPathComponent = [[bundle bundlePath] lastPathComponent];
    NSString *filepath = [NSString stringWithFormat:@"%@/%@.plist", [lastPathComponent stringByDeletingPathExtension], [bundle bundleIdentifier]];
    path  = [[paths objectAtIndex: 0] stringByAppendingPathComponent:filepath];
}

とかで

~/Library/Application Support/プラグイン名/プラグイン名.plist

というパスがとれます。Xcode のバージョンによって微妙に逆ドメインになったりならなかったりしてましたが。

あとは適当に NSMutableDictionary に値突っ込んで writeToFile:atomically: で書き込めばいいと思います。NSUserDefaults でもっとエレガントに書ける方法がある気がしないでもないですけど。

Key イベントって結構簡単にとれるんですね

NSWindow の sendEvent: とか Swizzling しないととれないのかと思ってました。

NSEvent
+ (id)addLocalMonitorForEventsMatchingMask:(NSEventMask)mask handler:(NSEvent* (^)(NSEvent*))block NS_AVAILABLE_MAC(10_6);

で自身のアプリがアクティブな時に登録した種類のイベントだけ handler が呼ばれます。昔、Swizzling で頑張ってたのにいつの間にかこんな便利な機能が足されてました。

ということで

みんなもどんどん Xcode Plugin を作って快適なコーディングをしましょう。 そして Xcode の Defaults は汚さずにいきましょう。