@_dynamicReplacementを動的にロードして挙動を差し替える

# <span>@</span>_dynamicReplacementを持つframeworkを動的にロードして実装を差し替える
Swiftは<span>@</span>_dynamicReplacementの仕組みを使い、dynamic修飾子が設定されたメンバの実装を差し替えることができます
この仕組みは、**SwiftUIのPreview**などに活用されているようです
そこで、<span>@</span>_dynamicReplacementの基本的な挙動を確認し、<span>@</span>_dynamicReplacementが設定された実装を含むモジュールがロードされたタイミングで実装が差し替わるという特徴を生かし、frameworkを動的に読み込むことで、動的に挙動を差し替えるのを試してみました

:::note warn
この記事は<span>@</span>_dynamicReplacementの挙動を確認することを目的としており、frameworkを動的にロードする仕組みをプロダクトに導入することを推奨するものではありません
:::

# <span>@</span>_dynamicReplacementの基本的な挙動の確認
Libクラスのtext関数の挙動を変えてみます
dynamicを付けて、差し替えられるように宣言します
とりあえず、このまま実行してボタンを押下すると当然、textは`Default`に変わります

```swift
public class Lib {
    public init() {}
    // dynamicを付けて、差し替えられるようにする
    public dynamic func text() -> String {
        "Default"
    }
}

struct ContentView: View {
    @State var text: String = "Hello, world!"
    var body: some View {
        VStack(spacing: 32) {
            Text(text)
            Button {
                text = Lib().text()
            } label: {
                Text("Update text")
            }
        }
    }
}
```

挙動を差し替えるために、<span>@</span>_dynamicReplacementを設定した`_text`関数を作成します
この状態で実行してボタンを押下すると、textは`Replaced`に変わります

```diff_swift
public class Lib {
    public init() {}
    // dynamicを付けて、差し替えられるようにする
    public dynamic func text() -> String {
        "Default"
    }
}

+ extension Lib {
+     @_dynamicReplacement(for: text())
+     public func _text() -> String {
+         "Replaced"
+     }
+ }
```

# frameworkを動的にロードして実装を差し替える
<span>@</span>_dynamicReplacementが設定された実装を含むモジュールがロードされたタイミングで実装が差し替わります
そこで、frameworkを動的に読み込むことで、動的に挙動を差し替えるのを試してみました

## やりたいこと
上記の`Lib`を`BaseLib.framework`に入れ、<span>@</span>_dynamicReplacementの実装を`ReplaceLib`に入れます
通常時は`Default`が表示されますが、`Load framework`ボタンを押下して`ReplaceLib.framework`を読み込むと、`Replaced`が表示されるように変わります

```diff_swift
+ import BaseLib

struct ContentView: View {
    @State var text: String = "Hello, world!"
    var body: some View {
        VStack(spacing: 32) {
            Text(text)
            Button {
                text = Lib().text()
            } label: {
                Text("Update text")
            }
+             Button {
+                 // ReplaceLib.frameworkをロードして、挙動を差し替える
+                 let frameworkURL = Bundle.main
+                     .url(forResource: "ReplaceLib", withExtension: "framework")!
+                     .appending(component: "ReplaceLib")
+                 let res = DynamicLinking.open(at: frameworkURL)
+                 print("res: \(res)")
+             } label: {
+                 Text("Load framework")
+             }
        }
    }
}

+ enum DynamicLinking {
+     static func open(at url: URL) -> Result<UnsafeMutableRawPointer, NSError> {
+         if let res = dlopen(url.path, RTLD_LAZY) {
+             return .success(res)
+         }
+         let err = dlerror().flatMap { String.init(cString: $0) } ?? "unknown error"
+         return .failure(.init(domain: err, code: -1))
+     }
+ }
```

## 動作している様子
最初はLib().text()の結果は、`Default`ですが、`Load framework`ボタンを押下すると、`Replaced`に変化していることがわかります

![dyrep.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/506353a1-be30-a962-6dce-578e80a1d973.gif)


## BaseLib.frameworkの実装

適当なframeworkターゲットを作成し、BaseLibと名付け、Libクラスを実装しました
Libクラスはdynamic修飾子を持ったtext関数を持っています
ビルドし、`Product` > `Show Build Folder in Finder`からビルドされたBaseLib.frameworkを取得します

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/e310562b-b880-1e71-37dd-4ab87fe73788.png" width="400">

```swift
public class Lib {
    public init() {}

    public dynamic func text() -> String {
        "Default"
    }
}
```

## ReplaceLib.frameworkの実装
BaseLibで実装したLibクラスのtext関数を差し替える<span>@</span>_dynamicReplacementを持つReplaceLib.frameworkを作成します
frameworkターゲットを作成し、ReplaceLibと名付けます

先ほどビルドしたBaseLib.frameworkを追加します
Framework in Frameworkを避けるためにFrameworks and Librariesには入れますが、Do Not Embedを選択しておきます
![スクリーンショット 2023-07-29 16.06.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/c8540937-9e41-404d-59c4-02c90d8a2c48.png)

次に、ReplaceLib.frameworkにLibのextensionを実装します

```swift
import BaseLib

extension Lib {
    @_dynamicReplacement(for: text())
    public func _text() -> String {
        "Replaced"
    }
}
```

最後に、動的に読み込むためにteamとbundle identifierを設定し、署名するようにしておきます
![スクリーンショット 2023-07-29 16.06.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/441de37d-a259-016b-7b96-f035e47127b9.png)

BaseLib.frameworkと同様に、ビルドし、`Product` > `Show Build Folder in Finder`からビルドされたReplaceLib.frameworkを取得します

## アプリターゲットの実装
アプリターゲットを作成し、上記でビルドした`BaseLib.framework`と`ReplaceLib.framework`を追加します
ただし、`BaseLib.framework`のみEmbed & Signします
![スクリーンショット 2023-07-29 16.32.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/d7e23828-f534-ff33-29e1-de1a5089c8f0.png)

一方で、`ReplaceLib.framework`はEmbedせずに、Copy Bundle Resourcesでコピーしておくようにします
![スクリーンショット 2023-07-29 16.35.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/07674dd7-21dc-0e78-cffa-53e538640d72.png)

最後にdlopenを使ってframeworkをロードします

```swift
import SwiftUI
import BaseLib

struct ContentView: View {
    @State var text: String = "Hello, world!"
    var body: some View {
        VStack(spacing: 32) {
            Text(text)
            Button {
                text = Lib().text()
            } label: {
                Text("Update text")
            }
            Button {
                let frameworkURL = Bundle.main
                    .url(forResource: "ReplaceLib", withExtension: "framework")!
                    .appending(component: "ReplaceLib")
                let res = DynamicLinking.open(at: frameworkURL)
                print("res: \(res)")
            } label: {
                Text("Load framework")
            }
        }
        .font(.title)
    }
}

enum DynamicLinking {
    static func open(at url: URL) -> Result<UnsafeMutableRawPointer, NSError> {
        if let res = dlopen(url.path, RTLD_LAZY) {
            return .success(res)
        }
        let err = dlerror().flatMap { String.init(cString: $0) } ?? "unknown error"
        return .failure(.init(domain: err, code: -1))
    }
}
```

これで、<span>@</span>_dynamicReplacementを使って動的に実装を差し替えることができました

# まとめ
<span>@</span>_dynamicReplacementの基本的な挙動を確認し、frameworkを動的に読み込むことで、動的に挙動を差し替えるのを試してみました
動的リンクライブラリを読み込むことで簡単に実装を差し替えることができました
SwiftUIのPreviewがViewを少し操作してもスグに反映される理由が少しわかった気持ちになりました
また、特にテストでのモックなどでも、活用できそうな仕組みだと思いました