takasekといいます。iOSDCにて、Xcode Source Editor Extensionの世界という発表をしました。
iOSDC
ですけどiOSの話は一切出てきません。
iOSDC での発表について
かいつまむと、以下のような話です。
Xcode8から導入されたXcode Source Editor Extensionは、思ったより簡単に作れるんですが、正攻法だとやれることが少ないです。
- 現在編集中のファイルのテキストバッファに読み書きする
- カーソル位置・文字選択状態を変更する
それだけしかできません。
Xcode Source Editor Extensionの世界は、高い壁に阻まれています。その壁を、どう登るのか。
- 壁(1)入出力の壁:
AppKit
の助けを借りればペーストボードや他アプリとの連携が可能 - 壁(2)言語機能の壁:
Process
を使ってある程度のunixコマンドを実行可能 - 壁(3)ネットワークの壁: ネットワークに繋ぎたいときはApp Sandboxで
Outgoing connections (Client)
にチェックを入れる - 壁(4)Sandboxの壁: Sandboxによって制限されているファイルシステムへのアクセス等を実現するためには
XPC
を仲介する- ただしApp Storeには出せなくなります。GitHubに公開して誰かのcloneを待つ、野良extensionとして生きる覚悟を。
- 壁(5)GUIの壁: GUIを出したければ、extensionから自身のApplicationを立ち上げて、そちらで処理をする
- App ⇄ extension間のデータの受け渡しにも色々方法がある
等々、色々やりようはあるのですよ、という発表でした。詳しくはスライド(SpeakerDeck)かセッション動画(Youtube)見て下さい。
壁(6) Xcode そのものの操作の壁
そんな発表を踏まえた今回は、さらにもうひとつの壁を登りましょう。
5つの壁を登った結果、extensionでやれることがずいぶんと増えました。
しかし強力になったのはプロジェクト外部との連携方法に過ぎません。情報の取得・書き出しに使えるのは、結局は、現在編集中のテキストバッファだけ。
現在のプロジェクトの情報などを取得したり、Xcodeをダイレクトに操作できればもっと良いのですが……。
と思っていたところ、実現する方法が見つかりました。それが、Scripting Bridgeです。
Scripting Bridge とは
Introduction to Scripting Bridge Programming Guide for Cocoa
A scriptable application is one that you can communicate with from a script or another application, enabling you to control the application and exchange data with it.
Cocoaアプリケーションの中には、 Apple Script
や他のアプリケーションによって操作可能なインタフェースを備えたものがあります。そして、Xcodeもそのひとつです。
このインタフェースにソースコード上でアクセスできるようにしてしまえば、我々の勝ちです。
extension上でScripting Bridgeを使うにあたっての注意点
extensionのコマンド内でダイレクトにScripting Bridgeにアクセスしようとすると、失敗します。どうやらSandboxのせいらしいです。
ということは、解決のためには、 壁(4)
で述べたとおり、 XPC Service
を仲介する必要があります。
Scripting Bridgeの前に… XPC Serviceを導入する
プロジェクトのTARGETSの追加から XPC Service
を選びます。
名前は XcodeHelper
とでもしましょうか。
Build Phasesで extension に xpc をコピーするのを忘れないようにしてください。
(自分は、これを知らずに長期間ハマりました)
Bridging-Headerを作成してBuild Settingsに設定すれば、Swiftで書かれたextensionのコマンドからでもXPCにアクセスできます。
XPCServiceを新規作成した時、何もしなくてもテンプレートとして、 upperCaseString(_:withReply:)
というメソッドが生えています。
SourceEditorCommand
に次のようなミニマムなコードを書いて、動作確認してみましょう。
let connection = NSXPCConnection(serviceName: "XcodeHelperのbundle identifier")
connection.remoteObjectInterface = NSXPCInterface(with: XcodeHelperProtocol.self)
connection.resume()
let xcode = connection.remoteObjectProxy as! XcodeHelperProtocol
let semaphore = DispatchSemaphore(value: 0)
xcode.upperCaseString("aaaa") { (str) in
invocation.buffer.lines.add(str!)
semaphore.signal()
}
_ = semaphore.wait(timeout: .now() + 10)
Run
すると黒いXcodeが立ち上がりますので、適当なソースコード上でextensionのコマンドを実行します。
エディタに AAAA
と追記されれば、XPCとの接続は成功です。
ようやく Scripting Bridge の使い方
Xcodeのスクリプティング・インタフェースを出力するには、以下のコマンドを叩くだけです。
$ sdef /Applications/Xcode.app | sdp -fh --basename Xcode
Xcode.h
が出力されました。 Objective-Cのヘッダファイルです。
これをXcodeに突っ込んでやりましょう。
これで準備は完了です。
Scripting Bridgeは、Swiftとの相性が悪いです。Obj-Cのヘッダを介してはいるもののその実装がないためです。Swiftから使うことも、ヘッダの定義を地道に protocol
に書き替えれば不可能ではありせんが、ここは大人しくObj-Cで書いてみます。XPC Serviceを新規作成した際のテンプレートもObj-Cなので、丁度いいです。
テンプレートの upperCaseString(_:withReply:)
の中身を書き替えましょう。(メソッド名と処理が合わなくなる? 許して!)
- (void)upperCaseString:(NSString *)aString withReply:(void (^)(NSString *))reply {
XcodeApplication *app = [SBApplication applicationWithBundleIdentifier: @"com.apple.dt.Xcode"];
XcodeWorkspaceDocument *workspaceDocument = app.activeWorkspaceDocument;
SBElementArray<XcodeProject *> *projects = workspaceDocument.projects;
XcodeProject *project = projects.firstObject;
SBElementArray<XcodeWindow *> * windows = app.windows;
XcodeWindow *window = windows.firstObject;
XcodeScheme *scheme = workspaceDocument.activeScheme;
NSString *response = [NSString stringWithFormat:
@"Xcode version: %@\nworkspaceDocument.file: %@\nproject.name: %@\nwindow.name: %@\nscheme.name: %@",
app.version, workspaceDocument.file, project.name, window.name, scheme.name
];
reply(response);
}
extensionを再実行し、コマンドを走らせると……
いとも簡単にXcode内部の情報が取れました。
また未検証ですが、情報を取得するだけでなく、Xcodeに対しての操作もある程度は可能なようです。
@protocol XcodeGenericMethods
- (void) closeSaving:(XcodeSaveOptions)saving savingIn:(NSURL *)savingIn; // Close a document.
- (void) delete; // Delete an object.
- (void) moveTo:(SBObject *)to; // Move an object to a new location.
- (XcodeSchemeActionResult *) build; // Invoke the "build" scheme action. This command should be sent to a workspace document. The build will be performed using the workspace document's current active scheme and active run destination. This command does not wait for the action to complete; its progress can be tracked with the returned scheme action result.
- (XcodeSchemeActionResult *) clean; // Invoke the "clean" scheme action. This command should be sent to a workspace document. The clean will be performed using the workspace document's current active scheme and active run destination. This command does not wait for the action to complete; its progress can be tracked with the returned scheme action result.
- (void) stop; // Stop the active scheme action, if one is running. This command should be sent to a workspace document. This command does not wait for the action to stop.
- (XcodeSchemeActionResult *) runWithCommandLineArguments:(id)withCommandLineArguments withEnvironmentVariables:(id)withEnvironmentVariables; // Invoke the "run" scheme action. This command should be sent to a workspace document. The run action will be performed using the workspace document's current active scheme and active run destination. This command does not wait for the action to complete; its progress can be tracked with the returned scheme action result.
- (XcodeSchemeActionResult *) testWithCommandLineArguments:(id)withCommandLineArguments withEnvironmentVariables:(id)withEnvironmentVariables; // Invoke the "test" scheme action. This command should be sent to a workspace document. The test action will be performed using the workspace document's current active scheme and active run destination. This command does not wait for the action to complete; its progress can be tracked with the returned scheme action result.
@end
@interface XcodeApplication : SBApplication
- (id) open:(id)x; // Open a document.
- (void) quitSaving:(XcodeSaveOptions)saving; // Quit the application.
- (BOOL) exists:(id)x; // Verify that an object exists.
@end
まとめ
如何だったでしょうか。
Scripting Bridgeを使えば、Xcodeを色々いじって楽しめそうですね。皆さんも是非、Xcode Extensionで遊んでみて下さい。
なお今回の調査にあたり、 @norio_nomura さんに多くのコツをご教授いただきました。ありがとうございました。
最後に宣伝
iOSDCの登壇をきっかけに、自分でXcode Source Editor Extensionを作ってストアで頒布してみました。
PasteTheTypeというextensionです。よかったら使ってみて下さい。