Swift5.1で追加されそうなDynamic Replacementについて

SwiftUIの機能説明に


Xcode can swap edited code directly in your live app with “dynamic replacement”, a new feature in Swift.


という言葉があり、気になっていたので探していたのですがどこにも記述がなく、どういった感じで実現しているのかわからなかったのですが、先日Swift Forumsにスレッドを見つけたのでメモ。

How does the hot-reloading work in XCode11? - Source Tooling - Swift Forums

なお、この機能は将来的に変更になる可能性が多分にあります。


Versions


  • Xcode 11.0 beta (11M336w)

  • swiftc: Apple Swift version 5.1 (swiftlang-1100.0.38.29 clang-1100.0.20.14)


使い方

使い方は非常に簡単です。

まずModuleAでdynamic修飾子をつけた関数fooを宣言します。

// ModuleA

public struct A {
public dynamic func original() -> String {
return "ModuleA"
}
}

次に別のModuleBで@_dynamicReplacement(for: original())をつけた拡張関数replacementを宣言します。

// ModuleB

public extension B {
@_dynamicReplacement(for: original())
public func replacement() -> String {
return "ModuleB"
}
}

あとはdlopen(ライブラリを動的に読み込む命令)なりなんなりでModuleBを読み込めば、A().original()ModuleBに置きかわります。

// ModuleBを読み込んでいないとき

print(A().original()) // → "ModuleA"
// ModuleBを動的に読み込み
_ = dlopen("ModuleB.dylib", 0)
print(A().original()) // → "ModuleB"


試してみた

スレッドにあるサンプルコードではswiftcを使ってコンパイルをしてたのですが、めんどくさかったとりあえずお手軽に置き換わるのか見たいので、iOS FrameworkプロジェクトをUnit Test込みで作り、以下のコードを書きました。

// SampleDynamicReplacement

public struct SampleObject {
public init() {
}

public dynamic func original() -> String {
"original"
}

public dynamic var foo: String {
"original property"
}
}

// SampleDynamicReplacementTest

extension SampleObject {
@_dynamicReplacement(for: original())
func replacement() -> String {
"replacement"
}

@_dynamicReplacement(for: foo)
var bar: String {
"replacement property"
}
}

class SampleDynamicReplacementTests: XCTestCase {
func testExample() {
XCTAssert(SampleObject().original() == "replacement")
XCTAssert(SampleObject().foo == "replacement property")
}
}

実際に実行してみるとテストが通ります。

関数でもプロパティでも置換できる模様。

ただSampleObject.fooletにしたら

Replaced accessor get for 'foo' is not explicitly defined

で怒られました。

しかし、

public private(set) dynamic var foo: String = "original property"

にしたらビルドが通ったので、なかなか危険な香りがしますね...!!(まあdynamicつけなければいい話なんですが)

また、extension側でreplacementの戻り値をIntなどoriginalに適合してない型に変えるとコンパイルエラーになるので、しっかり型チェックが働いていることがわかります。

ちなみに試しに

extension SampleObject {

@_dynamicReplacement(for: original())
func replacement() -> String {
"replacement"
}

@_dynamicReplacement(for: original())
func replacement2() -> String {
"replacement2"
}
}

としたところ、コンパイルは通り、テストには失敗しました。

replacementの宣言の順番を逆にしたらテストは通ったので置き換わるのは読み込んだ順なんですかね・・・?

ここもなかなか危険な香りが・・・!!


まとめ

簡単にですがDynamic Replacementについて紹介しました。

この機能だけで、(protocolでいいですが)テスト時のMock差し替え、SwiftUIなどのHot Reload、またAppleの規約的にアウトまたは実際にストア配布のバイナリではできないかもしれませんが、バイナリを直接アプリに送りつけることでReact Nativeなどで使えるHot Code Pushができるかもと思うと、夢が膨らみますね!