InjectionIII を使って Xcode, AppCode でも Hot Reload する

4 min read読了の目安(約4200字

InjectionIII とは?

ある時 Xcode の補完が効かなくなってしまい、AppCode で開発するか〜と思って AppCode で開発を行っていました。
しかし、AppCode だと Xcode Previews を見ることはできないため、細かい View の修正を行うために毎回シミュレータを起動し直さなければいけなかったりするのは微妙にストレスでした。

そこで、AppCode で Xcode Previews 的なことはできないかな?と思って調べてみたところ、すぐにその方法を見つけることができました。というか AppCode が公式的に InjectionIII というツールを推奨していました。

https://pleiades.io/help/objc/create-a-swiftui-application.html
https://github.com/johnno1962/InjectionIII

InjectionIII は Flutter などでいうところの Hot Reload のようなものを iOS 開発でも行えるようにするツールです。
一度導入して、シミュレータを立ち上げたままコードを変更すれば、再ビルドせずにシミュレータ上で動作しているアプリに変更が反映されます。
自分が試してみたところ、反映まで若干の時間がかかる・Reload の際に Top の View に戻ってしまうという若干の使いにくさはあったものの、開発中に毎回ビルドする必要がなくなるため、作業効率は結構上がりそうだと感じました。

InjectionIII を利用して Hot Reload している様子

AppCode のドキュメントに記載されている InjectionIII の導入方法に沿っても導入はできるのですが、若干古い方法だったので InjectionIII の README に書かれている導入方法を紹介します。(AppCode の導入方法はプレビュー用のコードをいじるというものだったのですが、プレビュー用のコードを毎回いじるのは結構手間なので、README に書かれている方法の方が良さそうかなと感じました。)

InjectionIII の導入方法

Xcode も AppCode も導入方法はさほど変わらないので、Xcode の場合かつ SwiftUI を使用している場合の導入方法について軽く説明しようと思います。

導入は以下の手順で行うことができます。

  1. SPM で Hot Reloading を導入する
  2. Build Phase で Run Script を追加する
  3. Debug 用の Other Linker Flags に -Xlinker -interposable を追加する
  4. 任意の箇所に InjectionIII 用のコードを追加する
  5. var body: some View の付近に毎回コードを追加する

それぞれ軽く説明していきます。

SPM で Hot Reloading を導入する

まずは SPM で Hot Reloading というパッケージを導入します。

https://github.com/johnno1962/HotReloading

Build Phase で Run Script を追加する

次に Build Phase に 以下のような Run Script を追加します(Run Script の名前は適当で問題ないです)。

if [ -d $SYMROOT/../../SourcePackages ]; then
    $SYMROOT/../../SourcePackages/checkouts/HotReloading/start_daemon.sh
elif [ -d "$SYMROOT"/../../../../../SourcePackages ]; then
    "$SYMROOT"/../../../../../SourcePackages/checkouts/HotReloading/fix_previews.sh
fi

Debug 用の Other Linker Flags に -Xlinker -interposable を追加する

ここまできたら、Hot Reloading の README 的には二回ほどビルドすると上記で設定した Run Script によってビルドターゲットの Other Linker Flags に -Xlinker -interposable が追加されるはずなのですが、自分の場合は追加されずでした(深い原因までは探っていません🙇‍♂️)。

そのため、自分の場合は手動で Other Linker Flags に -Xlinker -interposable を追加して対処しました。

任意の箇所に InjectionIII 用のコードを追加する

次に、任意の箇所に InjectionIII 用のコードを追加します。

グローバルに定義するため、どこでも良いのですが以下のコードを追加します。

#if DEBUG
import Combine

public let injectionObserver = InjectionObserver()

public class InjectionObserver: ObservableObject {
    @Published var injectionNumber = 0
    var cancellable: AnyCancellable? = nil
    let publisher = PassthroughSubject<Void, Never>()
    init() {
        cancellable = NotificationCenter.default.publisher(for:
            Notification.Name("INJECTION_BUNDLE_NOTIFICATION"))
            .sink { [weak self] change in
            self?.injectionNumber += 1
            self?.publisher.send()
        }
    }
}

extension View {
    public func eraseToAnyView() -> some View {
        return AnyView(self)
    }
    public func onInjection(bumpState: @escaping () -> ()) -> some View {
        return self
            .onReceive(injectionObserver.publisher, perform: bumpState)
            .eraseToAnyView()
    }
}
#else
extension View {
    public func eraseToAnyView() -> some View { return self }
    public func onInjection(bumpState: @escaping () -> ()) -> some View {
        return self
    }
}
#endif

var body: some View の付近に毎回コードを追加する

ここまできたらほぼ Hot Reload できる準備は整っています。
あとは View を作成する度に以下のようなコードを毎回追加しておいて、ビルドすれば Hot Reload が無事に動きます。

struct ExampleView: View {
  var body: some View {
    // View のコード(VStack は例)
    VStack {
      // ...
    }
    .eraseToAnyView() // これを追加する
  }
  
  // これも追加する
  #if DEBUG
  @ObservedObject var iO = injectionObserver
  #endif
}

毎回追加するのは少々面倒ではありますが、これで Hot Reload ができるようになります。
また、Hot Reload 用のコードを View 付近に追加するのも微妙ですがデバッグ時にしか影響しないので問題はないと思います。(それでも気になる方は AppCode のドキュメントを参考にプレビューコードに Hot Reload 用のコードを追加する方法を導入するのも良さそうです)

おわりに

InjectionIII は Xcode だけでなく AppCode でも利用できます。

Xcode Previews は時々原因不明のエラーでクラッシュしてしまったり、Resume が必要になってしまったりで View のプログラミングが捗るとはいえども、まだ不安定な部分がある印象です。

InjectionIII も触ってみた感じ、Flutter ほど快適な Hot Reload ができるというわけではありませんでしたが、シミュレータを一度起動して View をいじれば勝手にシミュレータ上のアプリにも反映されるので、かなり便利だと感じました(何もしなくても Hot Reload できるようになってほしい😢 )。