Xcode Source Editor Extension を使った Xcode プラグインの作り方

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Xcode Source Editor Extension

「Xcode Source Editor Extension」は、Xcode 8 から作れるようになった Xcode の Source Editor の機能拡張のことです。Xcode の Source Editor を拡張するような機能を、誰でも実装し提供することができます。

本記事では、プロジェクトを作成し簡単なコマンドを実装するところまでを取り上げます。

プロジェクトの作成

「Create a New Xcode project」を選びます。

xcode-source-editor-ext-00

Xcode Source Editor Extension は、Mac アプリの Extension として作ります。ということで「macOS」の「Cocoa Application」を選びます。

xcode-source-editor-ext-01

プロジェクト名を適当に決めます。これは Mac アプリのプロジェクトの名前になります。ここでは「SampleApp」とします。

xcode-source-editor-ext-02

プロジェクトの保存場所を任意のディレクトリにします。

xcode-source-editor-ext-03

プロジェクトができたら、ターゲットを追加します。

xcode-source-editor-ext-04

「macOS」の「Xcode Source Editor Extension」を選びます。ここでは「SampleExtension」とします。

xcode-source-editor-ext-05

「Active "SampleExtension" scheme?」と聞かれます。これは、ビルドやデバッグのときに Extension を有効化するかどうかを聞かれています。「Activate」を選びます。

xcode-source-editor-ext-07

これで Source Editor Extension を開発する準備が整いました。

ファイル構成

生成された Source Editor Extension のファイル構成を見てみましょう。

xcode-source-editor-ext-08

ファイル名 説明
SourceEditorExtension.swift Extension のコアとなる XCSourceEditorExtension のサブクラス
SourceEditorCommand.swift Extension が提供するコマンドの実装を行う XCSourceEditorCommand のサブクラス
Info.plist Extension の設定を定義する Plist ファイル

SourceEditorExtension は、いわば AppDelegate のようなものです。Extension が起動された時をハンドリングできたり、コマンドを定義できたりします(コマンドの定義は Info.plist でも定義可能)。

Extension で提供したいコマンドの数だけ XCSourceEditorCommand のサブクラスを作ります。自動生成されたままの状態では、SourceEditorCommand というコマンド定義用のクラスが1つだけ定義されています。

コマンドを実装してみる

いきなり実用的な Extension を作るのはハードルが高いので、まずは簡単なコマンドから実装してみましょう。

選択した行に、以下のコードを追加するコマンドを実装してみたいと思います。

let message = "Hello!"

SourceEditorCommand クラスに perform:with:completionHandler: メソッドを定義し、次のように実装します。

import Foundation
import XcodeKit

class SourceEditorCommand: NSObject, XCSourceEditorCommand {

    func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {
        let textBuffer = invocation.buffer

        let lines = textBuffer.lines
        let selections = textBuffer.selections

        guard let selection = selections.firstObject as? XCSourceTextRange else {
            completionHandler(NSError(domain: "SampleExtension", code: 401, userInfo: ["reason": "text not selected"]))
            return
        }

        let line = selection.end.line
        let end = selection.end.column

        let indentSpace = String(repeating: " ", count: end)
        lines.insert("\(indentSpace)let message = \"Hello!\"", at: line)

        completionHandler(nil)
    }

}

選択中の内容は、引数で渡されている XCSourceEditorCommandInvocationbuffer から取得できます。bufferXCSourceTextBuffer クラスで、selections には選択中の内容が配列で格納されています。firstObject を取得すると、1つ目の選択中の内容が取得できます。また、lines には行ごとの情報が格納されています。selectionslines も、NSMutableArray です。

selectionXCSourceTextRange クラスです。選択の範囲が表現されており、開始の行・列と終了の行・列が取得できます(行・列は XCSourceTextPosition クラス)。

選択が終わっている行を対象に、挿入したいコードを追加しています。最後には引数で渡されている completionHandler() を呼び出して終わりです。

デバッグ

デバッグするには、プロジェクト設定の「Signing」で「Enable Development Signing」をクリックする必要があります。Map アプリの Target と Extension の Target、両方で有効にします。

xcode-source-editor-ext-09

デバッグしようとすると、Extension の対象とするアプリを選択できます。もちろん Xcode ですね。

xcode-source-editor-ext-10

おぉ…なんか黒い…。

xcode-source-editor-ext-debug-01

iOS アプリの Xcode プロジェクトを開いてみます。本物の Xcode と、デバッグ中の Xcode が明確に見分けがつくよう、アプリアイコンとステータスウインドウが黒くなっています。

xcode-source-editor-ext-debug-02

実装した Source Editor Extension のコマンドは、メニューの「Editor」の一番下に表示されます。「SampleExtension」という名前にしたので「SampleExtension」が表示されます。その中にある「Source Editor Command」を選びましょう。

xcode-source-editor-ext-debug-03

選択している行・列の後に、コードが追加されました!

xcode-source-editor-ext-debug-04

まとめ

Mac アプリや iOS アプリの Extension を作ったことがある人は、非常に覚えやすい作りだと思います。うまく活用して Xcode をより多機能にしていきたいですね!

参考