こんばんは。
卒論とか学生最後のテストとかが忙しいのですが、気分転換にXcodeのプラグインの開発をしてみました。
Xcodeのプラグイン開発方法をググってみると分かると思うのですが、情報が少なく、特にXcode 6で開発した例が見つけられなかった(少なくとも日本語では)ので、紹介しようと思います。
ー 1.プラグインテンプレートの準備 ー
Xcodeのプラグイン開発はアップルが公式のサポートしていないので、アップルが公式に公開しているプラグイン開発方法はありません。
しかし、Xcodeが標準で提供する機能だけでは満足できない人にとってプラグインは必須!さらに自分でプラグインを開発できれば開発環境を自由自在にカスタマイズすることができるようになります!
Xcode 6で開発する方法は大きく分けて2種類あります。
・一から作成する方法
・テンプレートから作成する方法
今回紹介するのはテンプレートから作成する方法です。一から作成する方法もあるのですが、テンプレートから作成する方法の方が楽ですし、一から作成したからといってできることが増えるわけではないので、今回はテンプレートを使います。
まず、Xcodeのパッケージ管理用のプラグイン「Alcatraz」をインストールします。「Alcatraz」をインストールすることによって、Xcodeのプラグインをパッケージマネージャーから簡単にインストールできるようになります。
「Alcatraz」をインストールするにはターミナルに
$ curl -fsSL https://raw.github.com/supermarin/Alcatraz/master/Scripts/install.sh | sh
と入力して実行するだけです。
「Alcatraz」をインストールしたら、Xcodeを再起動します。するとメニューの「Window」に「Package Manager」が追加されていると思います。これで簡単にプラグインをインストールできるようになります。
今回インストールするプラグインは「Xcode Plugin」です。「Xcode 4 Plugin」ではないので注意をしてください。インストール方法は簡単で、プラグインの左に表示されているボタンをクリックするだけです。インストールが終了したらXcodeを再起動してください。
ー 2.プラグインの開発 ー
Xcodeを再起動したら、早速プラグインを開発してみましょう。プロジェクトの新規作成から「Xcode Plugin」を選択してください。進むとプロジェクトの名前の入力とプログラミング言語を選択する画面が表示されるので、テキトーに入力してください。今回、私はSwiftを選択したのでSwiftのコードを例に紹介します。
すると自動的に以下のコードが生成されると思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import AppKit var sharedPlugin: LL? class LL: NSObject { var bundle: NSBundle class func pluginDidLoad(bundle: NSBundle) { let appName = NSBundle.mainBundle().infoDictionary?["CFBundleName"] as? NSString if appName == "Xcode" { sharedPlugin = LL(bundle: bundle) } } init(bundle: NSBundle) { self.bundle = bundle super.init() createMenuItems() } deinit { NSNotificationCenter.defaultCenter().removeObserver(self) } func createMenuItems() { var item = NSApp.mainMenu!!.itemWithTitle("Edit") if item != nil { var actionMenuItem = NSMenuItem(title:"Do Action", action:"doMenuAction", keyEquivalent:"") actionMenuItem.target = self item!.submenu!.addItem(NSMenuItem.separatorItem()) item!.submenu!.addItem(actionMenuItem) } } func doMenuAction() { let error = NSError(domain: "Hello World!", code:42, userInfo:nil) NSAlert(error: error).runModal() } } |
ちなみに上記のコードだけでプラグインを動作させることが可能です。「Command + B」でビルドして、Xcodeを再起動してみてください。するとメニューのEditの一番下に「Do Action」が追加されていると思います。クリックすると「”Hello World”」とアラートが表示されると思います。
自動生成されたコードはひな形なので、これを基本にしてプラグインを開発することになります。といってもプラグイン開発に関する情報は少なく、どのようなコードを書けば良いのか分からないと思います。AppKitはOS Xのフレームワークなのですが、情報が少ないようです。ということで、私の開発したプラグインのコードを参考に、少しだけ紹介します。
私は、プロジェクト内のソースコード行数の総数をカウントする機能をXcodeに埋め込みたかったので開発してみました。基本的には以下のような処理を実装しています。
・プロジェクトのワークスペース(ディレクトリ)を取得する
・ワークスペース内の「.swift」拡張子のファイルを全て取得する(ワークスペース内の全てのディレクトリを探索します)。
・取得したファイルの行数をカウントして合計を計算する
・画面に行数を表示する
「プロジェクトのワークスペース(ディレクトリ)を取得する」以外は簡単に実装できたのですが、ワークスペースを取得する処理は「Swift」で書く方法が分からなかったので「Objective-C」を使いました。
以下は、ワークスペースを取得するクラスのヘッダーとモデルファイルです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#import <Foundation/Foundation.h> #ifndef LineCounter_Directory_h #define LineCounter_Directory_h @interface Directory : NSObject { NSString *_path; } - (NSString*)path; @end #endif |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#import <Foundation/Foundation.h> #import <AppKit/AppKit.h> #import "Directory.h" @implementation Directory - (NSString*)path { NSArray *workspaceWindowControllers = [NSClassFromString(@"IDEWorkspaceWindowController") valueForKey:@"workspaceWindowControllers"]; id workSpace; for (id controller in workspaceWindowControllers) { if ([[controller valueForKey:@"window"] isEqual:[NSApp keyWindow]]) { workSpace = [controller valueForKey:@"_workspace"]; } } NSString *workspacePath = [[workSpace valueForKey:@"representingFilePath"] valueForKey:@"_pathString"]; _path = workspacePath; if(_path != nil){ return _path; }else{ return @"none"; } } @end |
以下は、ファイル一覧を取得してソースコード行数を計算するSwiftのコードです。自動生成されたSwiftのコードをカスタマイズしただけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
import AppKit import Foundation var sharedPlugin: LineCounter? class LineCounter: NSObject { var bundle: NSBundle var codeList : [String] = [] var lines = 0 class func pluginDidLoad(bundle: NSBundle) { let appName = NSBundle.mainBundle().infoDictionary?["CFBundleName"] as? NSString if appName == "Xcode" { sharedPlugin = LineCounter(bundle: bundle) } } init(bundle: NSBundle) { self.bundle = bundle super.init() createMenuItems() } deinit { NSNotificationCenter.defaultCenter().removeObserver(self) } func createMenuItems() { let menu = NSMenu(title: "Utility") let actionMenuItem = NSMenuItem(title:"Count Line", action:"doMenuAction", keyEquivalent:"") actionMenuItem.target = self menu.addItem(actionMenuItem) let newMenuItem = NSMenuItem(title: "Utility", action: nil, keyEquivalent: "") newMenuItem.submenu = menu NSApp.menu!!.addItem(newMenuItem) } func doMenuAction() { let dir = Directory() let dirPath = dir.path() var result = "" if(dirPath == "none"){ return } searchDir(parsePath(dirPath)) for code in codeList{ lines += getLinesOfFile(code) } let alert = NSAlert() alert.messageText = "プロジェクト内ソースコード行数" alert.addButtonWithTitle("OK") alert.informativeText = String(lines) + "行" alert.runModal() codeList.removeAll(keepCapacity: false) lines = 0 } func searchDir(dirPath : String){ var fileStr = "" var isDir : ObjCBool = false var error : NSError? let manager = NSFileManager.defaultManager() let list = manager.contentsOfDirectoryAtPath(dirPath, error: nil)! for path in list { var result = manager.fileExistsAtPath(dirPath+(path as String), isDirectory: &isDir) if(result){ if(isDir){ if((path as String) != ".git"){ searchDir(dirPath + (path as String) + "/") } }else{ if(path.hasSuffix(".swift")){ codeList.append(dirPath + (path as String)) } } } } } func parsePath(path : String) -> String{ var i = 0 for(i=countElements(path)-1;i>=0;i--){ let start = advance(path.startIndex,i) let end = advance(path.endIndex, i-countElements(path)+1) let char = path.substringWithRange(Range<String.Index>(start: start, end: end)) if(char == "/"){ break } } let start = advance(path.startIndex,i+1) return path.substringToIndex(start) } func getLinesOfFile(path : String) -> Int{ var current = 0 let code = String(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: nil) code?.enumerateLines({line,stop in current+=1 }) return current } } |
ソースコード全部公開してしまいましたが、githubにも置いておきます。
https://github.com/rb-de0/line_counter
ではでは