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.foo
をlet
にしたら
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ができるかもと思うと、夢が膨らみますね!