14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

iOSAdvent Calendar 2019

Day 23

アクセス修飾子のつけ忘れを防ぐXcode Source Editor Extensionを作ってみた

Last updated at Posted at 2019-12-22

はじめに

iOS Advent Calendar 2019 23日目の記事となります。
iOSDC 2019にてこんなプロポーザルを出してみて結局作っていなかったので作ってみたという内容になります。

n番煎じながら、Xcode Source Editor Extensionのおさらい〜各種手順、今回自分で利用する用途としてごくごく必要最小限で実装したソースコードを載せつつできることの解説を少々、といった内容です。
Xcode Source Editor Extensionガチ勢な方には非常に物足りない内容かと思われますのでご了承くださいませ...

作ったもの

選択範囲のソースコードに対して指定してある文字列へ置換するExtensionです。ソースコードは後述。

zkwnq-fkesk.gif

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の探し方

上記の参考資料から抜粋

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) を新規作成

スクリーンショット 2019-12-19 16.38.32.png

2. TARGETS > +ボタンより Xcode Source Editor Extension を作成

スクリーンショット 2019-12-19 20.22.33.png スクリーンショット 2019-12-19 20.23.06.png

3. 作成したスキームはActivateする
スクリーンショット 2019-12-19 20.25.16.png

そうすると、以下のようにファイルが作成されます。

スクリーンショット 2019-12-22 18.16.10.png

4. SourceEditorCommand.swift へコマンド1個分のクラスがデフォルトで定義済みのため、 Info.plist へコマンドの名前を設定

スクリーンショット 2019-12-22 18.23.28.png

4を設定した状態でデバッグすると、XcodeのEditorメニュー項目へ実行時のコマンド名が設定したが表示されるようになります。
スクリーンショット 2019-12-22 18.25.10.png

5. SourceEditorCommand.swift の performメソッドへ実装

6. 2のスキーマを選択した状態で Product > Archive

7. 6のアプリを実行

8. Macのシステム環境設定 > 機能拡張 > Xcode Source Editor より、6の項目のチェックボックスをON
→Xcodeを再起動後、XcodeのEditorメニュー項目から選択できるようになる

実装

冒頭の作ったものの実装が以下。

SourceEditorCommand.swift
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 について

例:
スクリーンショット 2019-12-22 19.12.17.png

こちらのように選択している場合、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の手札が少し増えたかも!って思っていただけると嬉しいです。

14
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?