ファイルを保存するだけで動的にプログラムを更新できるInjectionIIIについて、macOSでもできるようにしてみた


以前、ファイルを保存するだけで動的にプログラムを更新できるInjectionIIIについての解説記事を投稿した。
その際は、対象がiOSだけだった。macOSでもできると書いてあったが、方法がわからず、ようやく少し進展した(まだ道半ば)。
https://qiita.com/KoichiroEto/items/5cb149a6e5d74bbdd66c

3. macOSのプログラムからインジェクションしてみる

これまでは、iOS用アプリをインジェクションしていた。iOSアプリはシミュレーター上で動作しており、macOSアプリはそうではないという違いがある。そのため、いくつか追加の手順が必要となる。

3.1. なにかアプリを作る

まず、さきほどと同様に、なにかシンプルなアプリを開発する。
Xcodeを起動→Create a new Xcode Project→macOS→「App」→Next→Product Name:「MacTest」、User Interface: Storyboard→Next→「~/dev」を指定→Create
ViewController.swiftに、以下のようにshow()を追加。viewDidLoad()から呼ばれるようにする。

ViewController.swift
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        show()
    }
    func show() {
        let label = NSTextField(frame: NSRect(x: Int.random(in: 10..<100), y: Int.random(in: 10..<100), width: 150, height: 50))
        label.backgroundColor = NSColor.cyan
        label.stringValue = "Hello, world!"
        view.addSubview(label)
    }
}

(TextFieldの位置がランダムなのは、諸事情がある。後述する。)
まずはこの段階で実行してみる。Cmd-R→ビルドされ、実行される。ウィンドウが表示され、「Hello, world!」が表示される。
ViewController.swiftに戻り、"Hello, world!""Hello, Japan!"に修正してCmd-Sで保存する。当然、何も反映されない。この時点ではまだインジェクションされていないからだ。
再度Cmd-Rすると、一旦アプリが終了し、再度立ち上げられ、"Hello, Japan!"が表示される。約3秒で立ち上がる。この速度ならあまり不満は持たれないかもしれない。
該当個所を、「Hello, world!」に戻しておく。インジェクションの設定を始めてみよう。

3.2. プロジェクトを設定する

Xcodeに戻る。
Cmd-1→MacTestのプロジェクトを選択→PROJECT: MacTest→Build Settings→Linking→Other Linker Flags→ここにカーソルを乗せると左に三角が表示されるので、それをクリックする→Debugの右の「+」をおす→Any Architecture | Any SDK:「-Xlinker -interposable」→リターンを押すと確定する
Cmd-1→MacTestのプロジェクトを選択→TARGETS: MacTest→Signing & Capabilities→All→App Sandoboxの右の小さな「×」を押して、消す
Cmd-1→MacTestのプロジェクトを選択→TARGETS: MacTest→Signing & Capabilities→All→Hardened Runtime→「Disable Library Validation」をcheck

3.3. Bundleを追加

AppDelegate.swiftにBundleを追加する。

AppDelegate.swift
        #if DEBUG
    Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle")?.load()
        #endif

参考までに、AppDelegate.swiftの該当するメソッド全体を示す。

AppDelegate.swift
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        #if DEBUG
        Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle")?.load()
        #endif
    }

3.4. injected()を追加

ViewController.swiftに、injectedというメソッドを追加する。

ViewController.swift
    @objc func injected() {
        show()
    }

参考までに、ViewController classの全体である。

ViewController.swift
import Cocoa
class ViewController: NSViewController {
    @objc func injected() {
        show()
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        show()
        NotificationCenter.default.addObserver(self, selector: Selector("injected"), name: NSNotification.Name(rawValue: "INJECTION_BUNDLE_NOTIFICATION"), object: nil)
    }
    func show() {
        let label = NSTextField(frame: NSRect(x: Int.random(in: 10..<100), y: Int.random(in: 10..<100), width: 150, height: 50))
        label.stringValue = "Hello, world!"
        label.backgroundColor = NSColor.cyan
        view.addSubview(label)
    }
    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }
}

3.5. InjectionIIIにProjectを指定する

InjectionIIIが起動されていなかったら、起動する。
Status menuのInjectionIIIから「Open project」を選択→「~/dev/MacTest」を選択→「Select Project Directory」

3.6. 起動する

Cmd-R→アプリが起動して、「Hello, world!」が表示される。
この状態で、Hello, world!を編集してみる。Cmd-Sで保存する。そうすると、即座にコンパイルされ、読み込まれ、classがreplaceされる。
また、injectedが呼ばれ、そこからshowが呼ばれる。
ただ、前のオブジェクトが残ってしまっている。そのため、以前のTextFieldは消去されない。そのまま残るだけである。
そのため、以前のversionでは場所が固定されているので、文字が変更されない。これが更新される方法は、これから調べる予定。

とりあえず、今日はここまで!