Objective-C で書いた Sketch Plugin を JavaScript で書き直した
1年半程前に開発した Sketch Plugin PluginsList を JavaScript(CocoaScript) で書き直しました。
何をするプラグイン?
PluginsList はインストールしている(厳密には有効化している)プラグインの情報をテキストとして出力してくれるプラグインです。例えばプロジェクトに新たに参画するデザイナー向けに「このプラグインを使っているからインストールしておいて」みたいなシチュエーションでサクッと一覧情報として出力できます。
現在対応しているのは以下の3フォーマット
- CSV
- Markdown Table
- Backlog Table
です。
- プラグイン名
- バージョン番号
- Web サイトの URL
を出力します。
CSV のサンプル
"Name", "Version", "URL"
"Flat Export", "2.3.5", "", "", "Adam Thompson"
"I Want Apple System Font", "0.0.4", "", "", "usagimaru"
"MarginSymbols", "0.4.2", "https://github.com/griffin-stewie/MarginSymbols", "", "griffin-stewie"
"Plugin List", "1.1.0", "https://github.com/griffin-stewie/PluginsList", "", "griffin-stewie"
"Prism2ColorVariables", "0.1.3", "https://github.com/griffin-stewie/Prism2ColorVariables", "", "griffin-stewie"
"Shared Style Finder", "1.2", "https://github.com/sonburn/shared-style-finder", "", "Jason Burns"
"Symbol Name Autocomplete", "0.5.5", "https://github.com/griffin-stewie/SymbolNameAutocomplete", "", "griffin-stewie"
"SymbolNameSync", "0.5.4", "https://github.com/griffin-stewie/SymbolNameSync", "", "griffin-stewie"
"TextStyleSearch", "0.5.0", "https://griffin-stewie.github.io/", "", "griffin-stewie"
"Unsplash", "1.1.1", "", "", "Sketch"
"arrows", "3.0.1", "https://spectrum.chat/sketch-arrows", "", "Farid Sabitov"
"🏃🏿 Runner", "1.1.8", "https://sketchrunner.com/release-notes", "", ""
Markdown Table のサンプル
| Name | Version | URL |
|--------------|------------|------------|
| Flat Export | 2.3.5 | |
| I Want Apple System Font | 0.0.4 | |
| MarginSymbols | 0.4.2 | https://github.com/griffin-stewie/MarginSymbols |
| Plugin List | 1.1.0 | https://github.com/griffin-stewie/PluginsList |
| Prism2ColorVariables | 0.1.3 | https://github.com/griffin-stewie/Prism2ColorVariables |
| Shared Style Finder | 1.2 | https://github.com/sonburn/shared-style-finder |
| Symbol Name Autocomplete | 0.5.5 | https://github.com/griffin-stewie/SymbolNameAutocomplete |
| SymbolNameSync | 0.5.4 | https://github.com/griffin-stewie/SymbolNameSync |
| TextStyleSearch | 0.5.0 | https://griffin-stewie.github.io/ |
| Unsplash | 1.1.1 | |
| arrows | 3.0.1 | https://spectrum.chat/sketch-arrows |
| 🏃🏿 Runner | 1.1.8 | https://sketchrunner.com/release-notes |
Backlog Table のサンプル
| Name | Version | URL |h
| Flat Export | 2.3.5 | |
| I Want Apple System Font | 0.0.4 | |
| MarginSymbols | 0.4.2 | https://github.com/griffin-stewie/MarginSymbols |
| Plugin List | 1.1.0 | https://github.com/griffin-stewie/PluginsList |
| Prism2ColorVariables | 0.1.3 | https://github.com/griffin-stewie/Prism2ColorVariables |
| Shared Style Finder | 1.2 | https://github.com/sonburn/shared-style-finder |
| Symbol Name Autocomplete | 0.5.5 | https://github.com/griffin-stewie/SymbolNameAutocomplete |
| SymbolNameSync | 0.5.4 | https://github.com/griffin-stewie/SymbolNameSync |
| TextStyleSearch | 0.5.0 | https://griffin-stewie.github.io/ |
| Unsplash | 1.1.1 | |
| arrows | 3.0.1 | https://spectrum.chat/sketch-arrows |
| 🏃🏿 Runner | 1.1.8 | https://sketchrunner.com/release-notes |
Sketch Plugin はそもそもどういう仕組みなのか?
歴史的経緯があるようで、初期は非公開 API を勝手に使うスタイルでプラグイン開発は行われていました。Sketch は基本的に Objective-C で書かれたソフトウェアです。Objective-C は動的な言語という特徴を持っていて、class-dump というツールを使ってソフトウェアで定義されているクラスやメソッドなどを抜き出すことができます。インターフェイスが分かれば動的にそのクラスを生成したりメソッドを呼び出すことが可能です。
このような下地があるところに Sketch 側が Plugin 機構としての manifest.json による定義や CocoaScript と呼ばれる JavaScript 風なのに Objective-C のメソッドも呼び出される実行環境を提供することで成立しています。
以前は非公開 API を使うのがスタンダードでしたが、現在では Sketch が公式な API を提供するようになり簡単な Plugin であれば JavaScript の世界のまま開発が可能になってきました。とは言え、まだまだ提供されている公式 API は十分と言えないところはあるので非公式 API と並行して利用することがあります。
Objective-C からの書き換え
Sketch Plugin で使われている CocoaScript には Objective-C で書かれた Framework をロードしてそこにあるクラスを利用することが可能です。
僕が開発した PluginsList も当初は以下の理由で Objective-C で書いた Framework を CocoaScript 側で呼び出すだけという構成になっていました。
- Objective-C は書き慣れた言語である
- CocoaScript 特有の書き方になじめなかった
- Objective-C のメソッドを呼び出す時に独特な記法やオブジェクトを使わないといけない場合がある
- コード補完が効かない
- JavaScript のオブジェクトを触っているのか Objective-C 由来のオブジェクトを触っているかが分かりにくく最悪 Sketch をクラッシュさせる
先にも書いたように最近では JavaScript 風に書ける環境が整ってきており将来的に公式 API に乗り換えられる時に乗り換えやすい状況にした方が開発が楽になると考えました。Objective-C で Framework を作って呼び出すスタイルを採用する場合、Objectiv-C, Foundation, AppKit の持つ力を直接利用できるメリットはありますが、環境の構築やヘッダファイルの準備など独自の余計な手順が多いのも書き換えの理由です。
書き換え
Plugin として提供する機能が元々のシンプルで Objective-C じゃないと実現できないことはないものだったので、基本的にはそっくりそのまま書き換えるだけで済みました。
唯一のはまりポイント
インストールされている Plugin の名前などをリスト化する際に名前でソートをするのに手間取りました。
ソートには NSMutableArray の - (void)sortUsingComparator:(NSComparator)cmptr;
を使っていたのでそれをそのまま使いたかったのですが、これが一筋縄にはいきませんでした。
CocoaScript では、Objective-C の Blocks を引数にとるメソッドの実行が素直に書けません。感覚的には Objective-C の Blocks は「関数を引数として渡しているようなもの」見えるので JavaScript 的にも Function をただ渡せば良いだけのように思えます。実際にそれをやると Sketch ごとクラッシュします。エラーメッセージもろくに出ないため原因をつかむのに時間がかかります。
ここ に書かれている情報を元に謎の記述をすれば Blocks を引数にとる Objective-C のメソッドを CocoaScript から直接呼び出せます。
実際には以下のようなコードです。
let plugins = manager.enabledPlugins();
plugins.sortUsingComparator(__mocha__.createBlock_function('v24@?0@8@16', (lhs, rhs) => {
const lhsName = NSString.stringWithString(lhs.name());
const rhsName = NSString.stringWithString(rhs.name());
const result = lhsName.compare(rhsName);
return result;
}))
__mocha__
が何か分かりません...
createBlock_function
の第1引数の v24@?0@8@16
とか暗号です...
ちなみにこの暗号はここにあるコードを自分が知りたい Blocks のシグネチャに改変して実行すると分かります。
無事に - (void)sortUsingComparator:(NSComparator)cmptr;
を呼び出せるようになったのですがソート結果が何故か Objective-C 時代と違っていて結局は普通に JavaScript のソートを使うことで同じソート結果が得られるようにしました。
全く無駄でした....
さいごに
自分自身以外に全く需要のない CocoaScript から Blocks を引数を持つ Objective-C のメソッドを呼び出す方法とインストール済みの Sketch Plugin の一覧を出力する自作 Plugin PluginsList の紹介でした。
ぜひ皆さんも Sketch Plugin の開発にチャレンジしてみてください。
Discussion