Xcodeの開発で、通常であれば、ファイルを保存し、ビルドし、プログラムを起動するという手順をとる。プログラムを起動した状態で、終了させずに、その中身を書き換えるということは、通常はできない。だが、injectionという仕組みを使うと、できるようになる。
1. 「InjectionIII」を使ってみる
John Holdsworth氏により、「InjectionIII」というアプリが公開されている。これを使うと、Injectionを簡単に実行できるようになっている。
https://github.com/johnno1962/InjectionIII
以下、使ってみる。
1.1. アプリをインストール
InjectionIIIアプリをインストールする。
https://apps.apple.com/jp/app/injectioniii/id1380446739?mt=12
インストールされたら、実行する→Status menuにInjectionIIIアプリのアイコンが表示されたことを確認する→「Help/README」を選ぶと、以下のページに接続される。
https://github.com/johnno1962/InjectionIII
1.2. サンプルをダウンロードする
以下、Injection IIIのホームページである。
http://johnholdsworth.com/injection.html
以下より、サンプルプログラムをダウンロードできる。
http://johnholdsworth.com/GettingStarted.zip
解凍し、「~/dev/GettingStarted/
」となるようにする。
1.3. Xcodeからサンプルを開く
GettingStarted.xcodeproj
をダブルクリックして起動。→Open
1.4. InjectionIIIアプリから、GettingStartedを接続する
Status menuのInjectionIIIから「Open project」を選択→「~/dev/GettingStarted/
」を選択→「Select Project Directory」
1.5. 実行する
Cmd-Rで実行→シミュレーターに「Master」と表示される→「+」を押すと、現在時刻が表示される。それをクリックする→現在時刻と「CHANGEME」が表示される。
Xcodeに戻る→Cmd-1→DetailViewController.swift
を選択→" CHANGEME"
の個所を、たとえば" CHANGED!"
に変更する→Cmd-Sでセーブとすると、即座に画面上の「CHANGEME」だったところが、「CHANGED!」に変わる。これがインジェクションである。
2. 独自のプログラムからインジェクションしてみる
2.1. なにかアプリを作る
まずは、なにかシンプルなアプリを開発する。文字が表示されるものが良い。
Xcodeを起動→Create a new Xcode Project→iOS→「Single View App」→Next→Product Name:「InjectionTest」、User Interface: Storyboard→Next→「~/dev」を指定→Create
ViewController.swiftに、以下のようにshow()を追加。viewDidLoad()から呼ばれるようにする。
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
show()
}
func show() {
let button = UIButton(frame: CGRect(x: 40, y: 100, width: 200, height: 100))
button.backgroundColor = .cyan
button.setTitle("Hello, world!", for: .normal)
button.setTitleColor(.black, for: .normal)
view.addSubview(button)
}
}
まずはこの段階で実行してみる。Cmd-R→ビルドされ、実行される。Simulatorが起動して、「Hello, world!」が表示される。
ViewController.swiftに戻り、試しに"Hello, world!"
を"Hello, Japan!"
に修正してCmd-Sで保存しても、反映されない。当然である。
再度Cmd-Rすると、一旦シミュレーター上のアプリが終了し、再度立ち上げられ、今度は"Hello, Japan!"
に修正されている。3秒程度で立ち上がる。通常は、このようにアプリを終了し、再読み込みするという手順で開発する。ビルドが早いので、3秒程度でこのサイクルは実行できる。実用上はあまり不満は持たれないかもしれない。
2.2. Linker Flagsを設定する
Xcodeに戻る。Cmd-1→InjectionTestのプロジェクトを選択→PROJECT: InjectionTest→Build Settings→Linking→Other Linker Flags→ここにカーソルを乗せると左に三角が表示される→クリックする→Debugの右の「+」をおす→Debug→Any Architecture | Any SDK:「-Xlinker -interposable」→リターンを押すと確定する
2.3. Bundleを追加
AppDelegate.swiftにBundleを追加する。
#if DEBUG
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
#endif
参考までに、AppDelegate.swiftの該当するメソッド全体である。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
#if DEBUG
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
#endif
return true
}
2.4. injected()を追加
ViewController.swift
に、injectedというメソッドを追加する。
@objc func injected() {
show()
}
参考までに、ViewController classの全体である。
class ViewController: UIViewController {
@objc func injected() {
show()
}
override func viewDidLoad() {
super.viewDidLoad()
show()
}
func show() {
let button = UIButton(frame: CGRect(x: 40, y: 100, width: 200, height: 100))
button.backgroundColor = .cyan
button.setTitle("Hello, world!", for: .normal)
button.setTitleColor(.black, for: .normal)
view.addSubview(button)
}
}
2.5. InjectionIIIにProjectを指定する
Status menuのInjectionIIIから「Open project」を選択→「~/dev/InjectionTest」を選択→「Select Project Directory」
2.6. 起動する
Cmd-R→Simulatorが起動して、「Hello, world!」が表示される。以下のように出力される。
💉 Injection connected 👍
💉 Watching /Users/eto/dev/InjectionTest/**
2.7. 編集する
ViewController.swiftで、該当する行を以下のように編集してみる。
button.setTitle("Hello, Japan!", for: .normal)
Cmd-Sで、ファイルをセーブする。そうすると、シミュレーター上の「Hello, world!」が、即座に(1秒以内くらいで)「Hello, Japan!」に変わる。以下のように出力される。
💉 *** Compiling /Users/eto/dev/InjectionTest/InjectionTest/ViewController.swift ***
💉 Loading .dylib ...
objc[31231]: Class _TtC13InjectionTest14ViewController is implemented in both /Users/eto/Library/Developer/CoreSimulator/Devices/97670822-70F9-46B8-87F7-5545DF54E516/data/Containers/Bundle/Application/82DDD3CB-9924-4E5C-BCCC-1AE2A8A9E3AD/InjectionTest.app/InjectionTest (0x107b53b40) and /var/folders/94/shwk5bk14l5fx43cggr_n04m0000gn/T/com.johnholdsworth.InjectionIII/eval106.dylib (0x110d9c280). One of the two will be used. Which one is undefined.
💉 Loaded .dylib - Ignore any duplicate class warning ^
💉 Injected 'ViewController'
💉 Replacing InjectionTest.ViewController.__allocating_init(coder: __C.NSCoder) -> Swift.Optional<InjectionTest.ViewController>
💉 Replacing InjectionTest.ViewController.__allocating_init(nibName: Swift.Optional<Swift.String>, bundle: Swift.Optional<__C.NSBundle>) -> InjectionTest.ViewController
💉 Replacing InjectionTest.ViewController.viewDidLoad() -> ()
💉 Replacing InjectionTest.ViewController.show() -> ()
💉 Replacing InjectionTest.ViewController.injected() -> ()
💉 Class ViewController has an @objc injected() method. Injection will attempt a "sweep" of all live instances to determine which objects to message. If this crashes, subscribe to the global notification "INJECTION_BUNDLE_NOTIFICATION" to detect injections instead.
内部で起こっていることを説明すると、ViewController.swiftを常にウォッチし、編集されたのを検知したら、即座にそれをCompileし、動的にロードし、メソッドを置き換えている。その後、injected()が呼ばれ、表示が切り替わる。このようにすると、プログラムを実行している間に、動的にメソッドを書き換えできるため、プログラム開発効率が高まると考えられる。
ここまでのファイルを、以下に置く。このまま実行できるはずである。
https://github.com/eto/InjectionTest
done!