はじめに
iOS Advent Calendar 2019 23日目の記事となります。
iOSDC 2019にてこんなプロポーザルを出してみて結局作っていなかったので作ってみたという内容になります。
n番煎じながら、Xcode Source Editor Extensionのおさらい〜各種手順、今回自分で利用する用途としてごくごく必要最小限で実装したソースコードを載せつつできることの解説を少々、といった内容です。
Xcode Source Editor Extensionガチ勢な方には非常に物足りない内容かと思われますのでご了承くださいませ...
作ったもの
選択範囲のソースコードに対して指定してある文字列へ置換するExtensionです。ソースコードは後述。
Xcode Source Editor Extension とは
Xcode上で現在表示しているソースコードへ読み書き選択などを一括でやるような用途で利用する場合が多い拡張機能です。
上記以外に、別ファイルの操作やインターネットへ情報を取得しにいってうんぬんを伴わない場合はApp Storeへリリースも可能。
Xcode8に追加以降、特にアップデートがないので2年前の資料のたいへんありがたい資料が現役で最新です。
簡単なものからテクニカルなものまで、2019年現在もこちらの資料を把握さえすれば作れてしまいます!!
Xcode Source Editor Extensionの世界(完全版) / 20170916 #iosdc
続・Xcode Source Editor Extensionの世界 〜XPCとScripting Bridge〜
すでにあるXcode Source Editor Extensionの探し方
上記の参考資料から抜粋
- App Store にて
xcode extension
と検索- Awesome native Xcode extensionsから探す
- GitHubのtopicでリポジトリ探索
2個目が見やすくてお勧めです。
例えば選択したenum型のswitch-case文を生成してくれたり、選択したプロパティ群をアルファベット順に昇順に並び替えてくれたりなど「あるとちょっと便利かも」と感じる素敵なExtensionが豊富にございます。
興味のある方は是非覗いてみると良さそう。
IntelliJ製のIDEを知っているとこの挙動はXcode側で公式サポートしてくれよ・・・って思うものも多々ありますが
ちなみに今回わたしが実装したのはprivateへ直書きで差し替えるものでして、それ以外のその他修飾子への書き換えも対応している素敵なXcode Source Editor Extensionがすでにございます。
https://github.com/zoejessica/AccessControlKitty
Xcode Source Editor Extensionの作り方
環境
Mac OS Catalina 10.15.2
Xcode 11.3
Swift 5.1
作成〜利用までの手順
1. Cocoa Application(Xcode 11.3ではApp
) を新規作成
2. TARGETS > +ボタンより Xcode Source Editor Extension
を作成
そうすると、以下のようにファイルが作成されます。
4. SourceEditorCommand.swift
へコマンド1個分のクラスがデフォルトで定義済みのため、 Info.plist
へコマンドの名前を設定
4を設定した状態でデバッグすると、XcodeのEditorメニュー項目へ実行時のコマンド名が設定したが表示されるようになります。
5. SourceEditorCommand.swift
の performメソッドへ実装
6. 2のスキーマを選択した状態で Product > Archive
7. 6のアプリを実行
8. Macのシステム環境設定 > 機能拡張 > Xcode Source Editor より、6の項目のチェックボックスをON
→Xcodeを再起動後、XcodeのEditorメニュー項目から選択できるようになる
実装
冒頭の作ったものの実装が以下。
import Foundation
import XcodeKit
class SourceEditorCommand: NSObject, XCSourceEditorCommand {
func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {
let buffer = invocation.buffer
guard let selections = buffer.selections as? [XCSourceTextRange] else {
completionHandler(nil)
return
}
replaceLines(buffer: buffer, selections: selections, completionHandler: completionHandler)
}
private func replaceLines(buffer: XCSourceTextBuffer, selections: [XCSourceTextRange], completionHandler: @escaping (Error?) -> Void) {
guard let lines = buffer.lines as? [String], selections.count > 0 else {
completionHandler(nil)
return
}
for selection in selections {
if selection.start.line == selection.end.line {
buffer.lines[selection.start.line] = replace(target: lines[selection.start.line])
break
}
var index = selection.start.line
while index <= selection.end.line {
buffer.lines[index] = replace(target: lines[index])
index += 1
}
}
completionHandler(nil)
}
private func replace(target: String) -> String {
let replaceWords = [
"class ": "final class ",
"let ": "private let ",
"var ": "private var ",
"lazy var ": "private lazy var ",
"enum ": "private enum ",
"struct ": "private struct "
]
for words in replaceWords {
guard let range = target.range(of: words.key) else {
continue
}
guard target.range(of: words.value) == nil else {
return target
}
return target.replacingCharacters(in: range, with: words.value)
}
return target
}
}
replaceメソッドへ差し替え前後の文字列を辞書型で定義して、replaceLinesメソッドで選択した行の文字列を確認して差し替え後の文字列が含まれていなければ差し替えてるだけです。
Xcode Source Editor Extensionでできること
func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {
こちらで受け取っているinvocation
に含まれている、以下の項目の書き換えです。
-
invocation.buffer.lines
- Xcode上で現在表示しているファイルに含まれる文字列
-
invocation.buffer.selections
- 現在の選択範囲
invocation.buffer.selections
について
こちらのように選択している場合、selectionsに含まれる値は以下のようなイメージ。
let selecton = invocation.buffer.selections.first
print("\(selecton.start.line),\(selecton.start.column)") // (6,0)
print("\(selecton.end.line),\(selecton.end.column)") // (9,18)
selections自体に選択している文字列が含まれているわけでなく、選択されている開始と終了位置が収納されている。
なので、選択範囲の文字列に対して何らかの操作を行う場合はinvocation.buffer.lines
について上記の開始・終了位置に基づいてループを回してあれこれすることになります。その一例が上記です。
さいごに
例えばUITableViewのよく使うオーバーライドメソッドを使いまわしたいとかだとCode Snippetで事足りますが、それでも事足りないことを補完するような機能として使えるイメージが沸いた!効率UPの手札が少し増えたかも!って思っていただけると嬉しいです。