Xcode プラグインを Xcode 拡張に変換する方法
by Khoa Pham
Xcode は iOS および macOS の開発者にとって不可欠な IDE です。 初期の頃から、カスタムプラグインのビルドとインストールができることで、生産性が大きく向上していました。 Apple がプライバシーに関する懸念から Xcode 拡張機能を導入して間もない頃でした。
私は、XcodeWay、XcodeColorSense、XcodeColorSense2、Xmas など、いくつかの Xcode プラグインや拡張機能を構築してきました。 それはやりがいのある経験でした。 多くのことを学び、かなりの生産性を得ることができました。 この投稿では、私がどのように Xcode プラグインを拡張機能に変換したか、また、その際に経験したことを説明します。 XcodeWay
I choose a lazy person to do a hard job. なぜなら、怠け者はそれをする簡単な方法を見つけるからです
私は、ビル・ゲイツの上記の引用がとても好きです。 私は、繰り返しの多い退屈な仕事は避けるようにしています。 同じ作業を繰り返していることに気づいたら、それを自動化するためのスクリプトやツールを書いています。 これを行うには時間がかかりますが、近い将来、もう少し怠惰になるでしょう。
オープンソースのフレームワークやツールを構築することへの関心に加えて、私は使用している IDE – 主に Xcode – を拡張するのが好きです。
私が最初に iOS 開発を始めたのは 2014 年のことです。 私は、現在のプロジェクトのコンテキストで Xcode からすぐに多くの場所に移動できる素早い方法を求めていました。
- Finder で現在のプロジェクト フォルダを開いてファイルを変更する
- ターミナルを開いてコマンドを実行する
- GitHub で現在のファイルを開いて仕事仲間にリンクをすばやく渡す
- あるいはテーマ、プラグイン、コード スニペット、デバイス ログなどの他のフォルダーを開く、などがよくあることでしょう。
Every bit of time we save each day counts.
Xcode プラグインを書いて、上記のすべてのことを Xcode 内部で実行できたらクールだろうと考えました。 他の人がやるのを待つのではなく、私は袖をまくり上げ、最初の Xcode プラグイン – XcodeWay – を書き、オープン ソースとして共有しました。
Xcode プラグインとは何ですか
Xcode プラグインは Xcode によって公式にサポートされていませんし、Apple によって推奨されているわけでもありません。 また、それらに関するドキュメントもありません。 Xcode プラグインについて知るには、既存のプラグインのソース コードといくつかのチュートリアルを参照するのが一番です。 Xcode は、起動時にこのフォルダーに存在するすべての Xcode プラグインを読み込みます。 プラグインはXcodeと同じプロセスで実行されるので、Xcodeと同じように何でもできます。 プラグインのバグは、Xcodeをクラッシュさせる原因となります。
Xcodeのプラグインを作るには、macOS Bundle
を作成し、NSObject
を継承したクラスを1つ作り、NSBundle
を受け入れる初期化子を持つようにします。
- このクラスをプラグインのメイン エントリ クラスとして宣言し、
- 実行時に UI コントロールを作成して Xcode インターフェイスに追加するので、このバンドルには UI がないこと
<key>NSPrincipalClass</key><string>Xmas</string><key>XCPluginHasUI</key><false/>
Xcodeプラグインの別の問題は、継続して DVTPluginCompatibilityUUIDs
更新しなければならないことです。 これは、Xcode の新バージョンが出るたびに変更されます。 アップデートしないと、Xcode はプラグインのロードを拒否します。
Xcode プラグインでできること
多くの開発者は、Sublime Text、AppCode、または Atom などの他の IDE で見つかった特定の機能がないため、Xcode プラグインを構築します。 唯一の制限は、私たちの想像力です。 Objective C Runtime を活用して、プライベートなフレームワークや関数を発見することができます。 そして、LLDBとシンボリックブレークポイントをさらに使って、実行中のコードを調査し、その動作を変更することができます。 また、スウィズリングを使って、実行中のコードの実装を変更することも可能です。 Xcode プラグインを書くのは大変です – 推測が多く、時にはアセンブリの十分な知識が必要です。 これは他のプラグインをインストールすることができ、基本的に xcplugin
ファイルをダウンロードし、これを Plug Ins
フォルダーに移動するだけです。
プラグインで何ができるかを知るために、いくつかの人気のプラグインを見ていきましょう。 364>
SCXcodeMiniMap
Sublime Text の MiniMap モードが恋しくなったら、SCXcodeMiniMap を使って Xcode エディター内に正しいマップパネルを追加することが可能です。
FuzzyAutocompletePlugin
version 9 以前、Xcode には適切な自動補完機能がありませんでした (単にプレフィックスに基づくものだったのです)。 そこで、FuzzyAutocompletePlugin が輝いたのです。 364>
KSImageNamed-Xcode
UIImageView
内にバンドル画像を表示するには、しばしば imageNamed
メソッドを使用します。 しかし、画像ファイルの名前を正確に覚えておくのは大変です。 KSImageNamed-Xcode は、そのお手伝いをします。
ColorSense-for-Xcode
開発中のもうひとつの悩みは、RGBA 色空間を使用する UIColor
で作業することです。 指定した色の視覚的なインジケータは得られず、手動でチェックを行うのは時間のかかることです。 幸いなことに、ColorSense-for-Xcode があり、使用されている色を表示し、カラー ピッカー パネルで簡単に正しい色を選択することができます。 Xcode でこの機能がない場合、LinkedConsole を使用できます。 これは、Xcode コンソール内でクリック可能なリンクを有効にし、そのファイルに即座にジャンプできるようにします。
The hard work behind Xcode plugins
Making an Xcode plugin is not easy.これは、Xcode プラグインを作るのは簡単ではありません。 macOS プログラミングの知識が必要なだけでなく、Xcode のビュー階層に深く潜る必要があります。 プラグインの作り方に関するチュートリアルはほとんどありませんが、幸運なことに、ほとんどのプラグインはオープンソースなので、それらがどのように動作するかを理解することができます。 私はいくつかのプラグインを作ったことがあるので、それらについて技術的な詳細を説明できます。
Xcode のプラグインは通常 2 つのプライベート フレームワークで行われます。 DVTKit
と IDEKit
です。 システムフレームワークは /System/Library/PrivateFrameworks
にありますが、Xcode が独占的に使用するフレームワークは /Applications/Xcode.app/Contents/
にあり、そこには Frameworks
、 OtherFrameworks
、 SharedFrameworks
があります。
Xcodeのアプリバンドルからヘッダーを生成できる class-dump というツールがありますが、これを使用すると、Xcodeのアプリバンドルからヘッダーを生成することができます。 クラス名とメソッドで、NSClassFromString
を呼び出して名前からクラスを取得できます。
Swizzling DVTBezelAlertPanel framework in Xmas
クリスマスはいつも私に特別感を与えてくれるので、デフォルトの警告ビューではなく、ランダムなクリスマス画像を表示するXmasを作ることにしました。 このビューをレンダリングするために使用されるクラスは、DVTKitフレームワーク内のDVTBezelAlertPanelです。 Objective C ランタイムでは、スウィズリングと呼ばれるテクニックがあり、実行中のクラスやメソッドの実装やメソッド シグネチャを変更したり切り替えたりすることができます。
class func swizzleMethods() { guard let originalClass = NSClassFromString("DVTBezelAlertPanel") as? NSObject.Type else { return }
do { try originalClass.jr_swizzleMethod("initWithIcon:message:parentWindow:duration:", withMethod: "xmas_initWithIcon:message:parentWindow:duration:") } catch { Swift.print("Swizzling failed") }}
私は当初、Swiftですべてを行うのが好きでした。 しかし、Swizzle init メソッドを Swift で使用するのは厄介なので、Objective C で行うのが最も早い方法です。そして、ビュー階層をたどり、NSPanel
内の NSVisualEffectView
を見つけて画像を更新するだけです。
Interacting with DVTSourceTextView in XcodeColorSense
私は主に hex カラーで作業するので、色を見る素早い方法を望んでいます。 そこで、XcodeColorSense を作成しました。これは、hex カラー、RGBA、および named カラーをサポートします。
アイデアは簡単です。 文字列を解析して、ユーザーが UIColor
に関連する何かを入力しているかどうかを確認し、その色を背景として小さなオーバーレイ ビューを表示します。 Xcode が使用するテキスト ビューは、DVTKit
フレームワークの DVTSourceTextView
型です。
func listenNotification() { NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(handleSelectionChange(_:)), name: NSTextViewDidChangeSelectionNotification, object: nil)}
func handleSelectionChange(note: NSNotification) { guard let DVTSourceTextView = NSClassFromString("DVTSourceTextView") as? NSObject.Type, object = note.object where object.isKindOfClass(DVTSourceTextView.self), let textView = object as? NSTextView else { return }
self.textView = textView}
異なる種類の UIColor
構造を検出できるように、Matcher アーキテクチャを用意しました。 firstRectForCharacterRange
を呼び出し、convertRectFromScreen
と convertRect
でいくつかのポイント変換を行うことで位置を決定します。
Using NSTask and IDEWorkspaceWindowController in XcodeWay
Finally, my beloved XcodeWay.
I found myself to go to different places from Xcode with the context of current project.を参照ください。 そこで、XcodeWay をプラグインとして構築し、Window
の下に便利なメニュー オプションをたくさん追加しました。
プラグインは同じ Xcode プロセスで動作するので、メイン メニュー NSApp.mainMenu?.itemWithTitle("Window")
にアクセスすることができます。 そこで、メニューを変更することができます。 XcodeWay は Navigator プロトコルによって簡単に機能を拡張できるように設計されています。
@objc protocol Navigator: NSObjectProtocol { func navigate() var title: String { get }}
Provisioning Profile ~/Library/MobileDevice/Provisioning Profiles
や User data Developer/Xcode/UserData
のように固定パスを持つフォルダーについては、URL
を構築して NSWorkspace.sharedWorkspace().openURL
を呼び出すだけでよいでしょう。
Finder で現在のプロジェクトのフォルダーを開くにはどうすればよいですか? 現在のプロジェクトのパスの情報は、 IDEWorkspaceWindowController
の中に保存されています。 これは、Xcodeのワークスペースのウィンドウを管理するクラスです。
self.IDEWorkspaceWindowControllerClass = objc_getClass("IDEWorkspaceWindowController");
NSArray *workspaceWindowControllers = ;
id workSpace = nil;
for (id controller in workspaceWindowControllers) { if ( isEqual:]) { workSpace = ; }}
NSString * path = valueForKey:@"_pathString"];
最後に、valueForKey
を使用して、存在すると思われるプロパティの値を取得することができます。 この方法では、プロジェクトのパスを取得するだけでなく、開いているファイルへのパスも取得します。 したがって、activateFileViewerSelectingURLs
を NSWorkspace
で呼び出すと、そのファイルが選択された状態で Finder を開くことができます。
現在のプロジェクト フォルダーで、ターミナル コマンドを実行したい場合が多々あります。 これを実現するには、起動パッド /usr/bin/open
と引数 を持つ
NSTask
を使用できます。 iTerm がおそらく設定されていれば、これを新しいタブで開きます。
iOS 7 アプリのドキュメントは Application Support 内の固定ロケーション iPhone Simulator
に置かれます。 しかし、iOS 8 からは、すべてのアプリが固有の UUID を持ち、そのドキュメント フォルダを予測するのは困難です。
~/Library/Developer/CoreSimulator/Devices/1A2FF360-B0A6-8127-95F3-68A6AB0BCC78/data/Container/Data/Application/
マップを構築してトラッキングを行って現在のプロジェクトの生成 ID を見つけるか、各フォルダ内の plist をチェックしてバンドル ID を比較することができます。
私が思いついた迅速な解決策は、最も新しく更新したフォルダを検索することでした。 プロジェクトをビルドしたり、アプリの内部に変更を加えたりするたびに、そのドキュメント フォルダーは更新されます。 そこで、NSFileModificationDate
を使用して、現在のプロジェクトのフォルダーを見つけることができます。 Xcode プラグインで作業する場合、多くのハックがありますが、その結果は報われます。 毎日節約する数分ごとに、全体として多くの時間を節約することになります。
セキュリティと自由
大きな力には大きな責任が伴います。 プラグインが何でもできるという事実は、セキュリティに対する警鐘を鳴らしています。 2015年末、XcodeGhostと呼ばれるXcodeの改変版を配布し、Xcode Ghostで作られたアプリに悪意のあるコードを注入するマルウェア攻撃がありました。 このマルウェアは、とりわけプラグイン機構を使用していると考えられています。
私たちが Appstore からダウンロードする iOS アプリと同様に、Xcode などの macOS アプリは、Mac Appstore からまたは Apple 公式ダウンロード リンクを通じてダウンロードする際に Apple によって署名されます。
アプリにコード署名すると、それが周知のソースからで、最後に署名されてからアプリは変更されていないとユーザーに保証されます。 アプリがアプリのサービスを統合したり、デバイスにインストールしたり、App Store に提出したりする前に、Apple
が発行した証明書で署名する必要があります。このような潜在的なマルウェアを避けるために、WWDC 2016 で Apple は、Xcode にサードパーティの拡張機能をロードする唯一の方法として、Xcode Source Editor Extension を発表しました。 つまり、Xcode 8 からは、プラグインを読み込むことはできません。
Source Editor Extension
拡張機能は、制限された方法で安全に機能性を追加するための推奨アプローチです。
アプリ拡張機能により、ユーザーは iOS および macOS 全体でアプリの機能性とコンテンツにアクセスすることができるようになります。 たとえば、アプリを Today 画面のウィジェットとして表示したり、アクション シートに新しいボタンを追加したり、写真アプリ内で写真フィルターを提供したり、新しいシステム全体のカスタム キーボードを表示したりできるようになります。
今のところ、Xcode の拡張機能は Source Editor のみで、ソース ファイルのコンテンツの読み取りと変更、およびエディター内の現在のテキスト選択の読み取りと変更を行うことができます。 これは、現在のドキュメント コンテンツを変更するために XCSourceEditorCommand
に準拠する以外の方法で Xcode を変更できないという点で良いことです。
protocol XCSourceEditorCommand {
func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void)}
Xcode 8 には、新しいコード補完機能、Swift イメージとカラー リテラル、スニペットなどの多くの改良点があります。 そのため、多くの Xcode プラグインが非推奨となりました。 XVimのような必須プラグインにとって、これは人によっては耐え難いことです。
Unless you resign Xcode
Xcode 8 からのプラグインの制限を回避する回避策は、既存の Xcode 署名を resign という手法で置き換えることです。 resignは非常に簡単で、自己署名証明書を作成し、codesign
コマンドを呼び出すだけでよいのです。 この後、Xcode はプラグインをロードできるようになります。
codesign -f -s MySelfSignedCertificate /Applications/Xcode.app
しかしながら、署名が Xcode の公式バージョンと一致しないため、辞任した Xcode で構築したアプリを提出することは不可能です。 364>
Moving to Xcode extension
Xcode extension が良いので、プラグインを extension に移動し始めました。 Xmasについては、ビュー階層を変更するため、エクステンションにはなりません。
XcodeColorSense2のカラーリテラル
カラーセンスは、エクステンションを一から書き直して、XcodeColorSense2という名前にしています。 これはもちろん、現在のエディタビューの上にオーバーレイを表示することはできません。 そこで、Xcode 8+ で見つかった新しい Color literal
を利用することにしました。
色は小さなボックスで表示されます。 似たような色だと見分けがつかないかもしれないので、そのために名前も入れています。
func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void { guard let selection = invocation.buffer.selections.firstObject as? XCSourceTextRange else { completionHandler(nil) return }
let lineNumber = selection.start.line
guard lineNumber < invocation.buffer.lines.count, let line = invocation.buffer.lines as? String else { completionHandler(nil) return }
guard let hex = findHex(string: line) else { completionHandler(nil) return }
let newLine = process(line: line, hex: hex)
invocation.buffer.lines.replaceObject(at: lineNumber, with: newLine)
completionHandler(nil) }}
ほとんどの機能は、私のフレームワーク Farge に組み込まれていますが、Xcode 拡張機能の中でフレームワークを使用する方法は見つかりません。 たとえば、私は Cmd+Ctrl+S
を選択して、カラー情報を表示/非表示にしています。
これはもちろん、オリジナルのプラグインと比較して直感的ではありませんが、ないよりはましです。
How to debug Xcode extensions
extensions working and debugging is straightforward. Xcode を使用して Xcode をデバッグすることができます。 Xcode のデバッグされたバージョンには、グレーのアイコンがあります。
How to install Xcode extensions
The extension must have an accompanying macOS app. これは、Mac Appstore に配布することも、自己署名することも可能です。 この方法については、記事を書きました。
アプリのすべての拡張機能は、「システム環境設定」を通じて明示的に有効にする必要があります。
Xcode 拡張は今のところエディターでしか動作しないので、Editor
メニューが効果を発揮するためにはソース ファイルを開く必要があります。
AppleScript in XcodeWay
Xcode 拡張では NSWorkspace
, NSTask
, private class 構築はもう機能しない。 FinderGo で Finder Sync Extension を使用したことがあるので、同じように Xcode extension の AppleScript スクリプトを試してみようと思いました。
AppleScript は Apple によって作成されたスクリプト言語である。 ユーザーは、macOS 自体の一部と同様に、スクリプト可能な Macintosh アプリケーションを直接制御することができます。 繰り返しの作業を自動化したり、複数のスクリプト可能なアプリケーションの機能を組み合わせたり、複雑なワークフローを作成するために、スクリプト (記述された命令のセット) を作成できます。
AppleScriptを試すには、macOS内部に組み込まれているアプリ「Script Editor」を使用して、プロトタイプ関数を記述できます。 関数宣言は on
で始まり、end
で終わります。 システム関数とのコンフリクトを避けるため、私は通常my
を接頭辞として使用します。 以下は、ホームディレクトリを取得するために System Events に依存する方法です。
ユーザーインターフェイススクリプトの用語は、”System Events” スクリプト辞書の “Processes Suite” にあります。 このスイートには、次のようなほとんどのタイプのユーザー インターフェイス要素との対話のための用語が含まれています:
- windows
- ボタン
- チェックボックス
- メニュー
- ラジオボタン
- テキスト・フィールド。
システム イベントでは、process
クラスは実行中のアプリを表します。
多くの優れた市民アプリは、その機能の一部を公開して AppleScript をサポートしており、他のアプリで使用することが可能になっています。 以下は、Lyrics で Spotify から現在の曲を取得する方法です。
tell application "Spotify" set trackId to id of current track as string set trackName to name of current track as string set artworkUrl to artwork url of current track as string set artistName to artist of current track as string set albumName to album of current track as string return trackId & "---" & trackName & "---" & artworkUrl & "---" & artistName & "---" & albumNameend tell
特定のアプリの可能なコマンドをすべて取得するには、スクリプト エディタで辞書を開くことができます。 そこで、どの関数とパラメーターがサポートされているかを知ることができます。
Objective C が難しいと思っているなら、AppleScript はもっと難しいでしょう。 構文は冗長で、エラーが発生しがちです。 参考までに、XcodeWayを動かしているスクリプトファイル全体を紹介します。
あるフォルダを開くには、POSIX file
を使ってFinder
と指示します。
on myOpenFolder(myPath)tell application "Finder"activateopen myPath as POSIX fileend tellend myOpenFolder
次に、AppleScript を macOS のアプリや拡張機能の中で実行するには、正しいプロセスのシリアル番号とイベント識別子を含む AppleScript 記述子を作成する必要があります。 多くの場合、デバッグ中のファイルのリンクをリモートのチームメイトと共有したいのですが、そうすれば私がどのファイルを参照しているのかがわかります。 これは、AppleScript
内で shell script
を使用することで可能です。 .
on myGitHubURL()set myPath to myProjectPath()set myConsoleOutput to (do shell script "cd " & quoted form of myPath & "; git remote -v")set myRemote to myGetRemote(myConsoleOutput)set myUrl to (do shell script "cd " & quoted form of myPath & "; git config --get remote." & quoted form of myRemote & ".url")set myUrlWithOutDotGit to myRemoveSubString(myUrl, ".git")end myGitHubURL
では、quoted
と文字列の連結を使用して文字列を形成することができます。 幸運なことに、私たちはFoundation
フレームワークと特定のクラスを公開することができます。 ここでは、既存の機能をすべて利用するために、NSString
をどのように公開するかを説明します。
use scripting additionsuse framework "Foundation"property NSString : a reference to current application's NSString
これで、文字列操作のための他の関数を構築することができます。
on myRemoveLastPath(myPath)set myString to NSString's stringWithString:myPathset removedLastPathString to myString's stringByDeletingLastPathComponentremovedLastPathString as textend myRemoveLastPath
XcodeWayがサポートするクールな機能の1つは、シミュレータで現在のアプリケーションのドキュメントディレクトリに移動する機能です。 これは、保存されたデータやキャッシュされたデータをチェックするために、ドキュメントを検査する必要があるときに便利です。 ディレクトリは動的なので、検出するのは難しいです。 しかし、最近更新されたものからディレクトリをソートすることができます。 以下は、複数の shell scripts
コマンドを連結してフォルダを見つける方法です。
on myOpenDocument()set command1 to "cd ~/Library/Developer/CoreSimulator/Devices/;"set command2 to "cd `ls -t | head -n 1`/data/Containers/Data/Application;"set command3 to "cd `ls -t | head -n 1`/Documents;"set command4 to "open ."do shell script command1 & command2 & command3 & command4end myOpenDocument
この機能は、動画やダウンロードした画像が正しい場所に保存されているかどうかをチェックする Gallery を開発する際に大いに役立ちました。 スクリプトは、1993 年以来、常に macOS の一部でした。 しかし、Mac Appstore の登場とセキュリティの懸念により、AppleScript は 2012 年半ばについに制限されるようになりました。 364>
App Sandbox
App Sandboxは、macOSで提供されているアクセス制御技術で、カーネルレベルで実施されます。 アプリが侵害された場合に、システムやユーザーのデータへの被害を抑制するように設計されています。 Mac App Store を通じて配布されるアプリは、App Sandbox を採用しなければなりません。
Xcode 拡張機能が Xcode によって読み込まれるには、App Sandbox もサポートしなければなりません。
App Sandbox 実施当初は、App Sandbox Temporary Exception を使用してアプリに Apple Script へのアクセス許可を一時的に与えることが可能でした。
AppleScript を実行する唯一の方法は、それが ~/Library/Application Scripts
フォルダ内に存在する場合です。
カスタム スクリプトのインストール方法
macOS アプリや拡張機能は単独でアプリケーション スクリプトにスクリプトをインストールできない。
これを行うための 1 つの可能な方法は、Read/Write
を有効にして、NSOpenPanel
を使用してユーザーにスクリプトをインストールするフォルダーを選択するよう求めることです。
XcodeWay では、ユーザーが素早くインストールできるようにインストールシェルスクリプトを提供することにしました。 拡張機能のように、スクリプトはプロセス間通信に XPC を使用して別のプロセスで非同期に実行されます。 これは、スクリプトが私たちのアプリや拡張機能へのアドレス空間にアクセスできないため、セキュリティを強化します。
More security in macOS Mojave
今年、WWDC 2018で、Appleは多くのセキュリティ強化に焦点を当てたmacOS Mojaveを発表しました。 Your Apps and the Future of macOS Securityでは、macOSアプリの新しいセキュリティ要件について詳しく知ることができます。 その1つがAppleEvents.
unable to load info.plist exceptions (egpu overrides)
iOSではフォトライブラリ、カメラ、プッシュ通知など多くの権限に対して使用状況の記述を行うようになっていました。 今度は、AppleEvents.
この拡張機能が初めて AppleScript コマンドを実行しようとすると、ユーザーの同意を求めるために上記のダイアログが表示されます。 ユーザーは許可を与えることも拒否することもできますが、Xcode では「はい」と答えてください。
私たちの修正は、アプリ ターゲットで NSAppleEventsUsageDescription
と宣言することです。 拡張ターゲットではなく、アプリターゲットで宣言する必要があります。
<key>NSAppleEventsUsageDescription</key><string>Use AppleScript to open folders</string>
ここからどこへ行くか
ハァハァ、ふぅ このような長い旅にお付き合いいただきありがとうございました。 フレームワークやツールの作成には多くの時間がかかります。特にプラグインや拡張機能は、新しいオペレーティング システムやセキュリティ要件に適合させるために継続的に変更する必要があります。 しかし、より多くを学び、貴重な時間を節約するツールを手に入れたので、やりがいのあるプロセスです。
参考までに、完全にオープン ソースの私の拡張機能を以下に示します。