UIViewRepresentableの処理を親から呼び出す方法

## 背景

UIKit の時代では、下記のサンプルコードのように、子ビューの処理を親ビューやビューコントローラーから呼び出す設計が多々あるかと思います：

```swift:ChildView.swift
final class ChildView: UIView {
    // ...
    func refresh() {
        // do some refreshing task
    }
}
```

```swift: ParentViewController.swift
final class ParentViewController: UIViewController {
    @IBOutlet var childView: ChildView!
    // ...
    @IBAction func refreshChildView(sender: UIButton) {
        childView.refresh()
    }
}
```

上記の例では、`ParentViewController` は `ChildView` のほかに、ボタンも持っており、ボタンをタップした際に `childView.refresh()` を呼び出すように設定しています。

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63998/13187e76-a405-664e-6e16-9ec2ca4f3ab1.gif)

## 課題

ところが、同じような設計を SwiftUI で作ろうとすると、問題が起きます。SwiftUI の `View` はただの描画レシピであって、子ビューのインスタンスを取得できないから、そのインスタンスのメソッドも当然ながらそのまま呼び出せないです。

## 解決策

### その1

まずは一つの Workaround として、トリガー用の状態を作る方法を思いつく人も多いかと思います：

```swift:ChildView.swift
struct ChildView: View {
    var refreshingID: UUID
    var body: some View {
        SomeViewContents()
            .onChange(of: refreshingID) { _, _ in // refreshingID の値に意味はない
                refresh()
            }
    }
    // ...
    func refresh() {
        // do some refreshing task
    }
}
```

```swift:ParentView.swift
struct ParentView: View {
    @State private var refreshingID = UUID()
    var body: some View {
        HStack {
            ChildView(refreshingID: refreshingID)
            Button("refresh") {
                refreshingID = UUID()
            }
        }
    }
}
```

この方法はやりたいこととしてはできなくはないですが、慣れてない人からしてみたらやはり `refreshingID` の存在がなんか紛らわしいし、それと `refresh()` の関係も直感的とは言いにくいと思います。そして何より、今の `ChildView` は普通の `View` だからまだ `onChange(of:)` が使えて楽ですが、タイトルのような `UIViewRepresentable` なら `onChange(of:)` は使えないので、`Coordinator` の中で親から受け取った `refreshingID` を保持して比較処理を書かないといけないから、まあ書きやすいとは言い難いです。

### その2

次に思いつきやすい方法は、そもそも `refresh` 処理自体を親に持たせるよう作ることかと思います：

```swift:ParentView.swift
struct ParentView: View {
    var body: some View {
        HStack {
            ChildView()
            Button("Refresh") {
                refresh()
            }
        }
    }
    // ...
    func refresh() {
        // do some refreshing task
    }
}
```

これならそもそも `ChildView` がどんなものかあまり関係なくなってしまいますので、見た目もスッキリするし、こう書けるならこれに越したことはないかもしれませんが、設計上 `refresh` 処理を `ChildView` スコープ内に閉じたかったり、特に `UIViewRepresentable` の場合はそもそもその処理が本来の `UIView` の中にあって親に移動できないことも多々あると思います。なのでやはり親から `refresh()` を呼び出したいだけで、その処理の実装はできれば避けたいです。

### その3

肌感覚的にそんなに知られていないようですが、実は子供の状態を親に伝搬するのに、SwiftUI には Preference と言う仕組みもあります。なので「処理」自身も一種の「状態」とみなせば、このようなやり方もあるかと思います：

```swift:RefreshAction.swift
struct RefreshAction: Equatable {
    private var childView: ChildView?
    init(childView: ChildView?) {
        self.childView = childView
    }
    static func == (lhs: Self, rhs: Self) -> Bool {
        // 実際の実装では `childView` ではなく何かしらオブジェクトを持つことが多いかと思いますので、この例のような無理矢理な比較ではなくそのままオブジェクトインスタンスの同一比較すればいいと思います
        switch (lhs.childView, rhs.childView) {
        case (nil, nil),
             (.some, .some):
            return true
        default:
            return false
        }
    }
    func callAsFunction() {
        childView?.refresh()
    }
}

struct RefreshActionKey: PreferenceKey {
    static var defaultValue: RefreshAction = .init(childView: nil)
    static func reduce(value: inout RefreshAction, nextValue: () -> RefreshAction) {
        value = nextValue()
    }
}
```

```swift:ChildView.swift
struct ChildView: View {
    var body: some View {
        SomeViewContents()
            .preference(key: RefreshActionKey.self, value: .init(childView: self))
    }
}
```

```swift:ParentView.swift
struct ParentView: View {
    @State private var refresh: RefreshAction?
    var body: some View {
        HStack {
            ChildView()
            Button("Refresh") {
                refresh?()
            }
        }
        .onPreferenceChange(RefreshActionKey.self) { value in
            refresh = value
        }
    }
}
```

このやり方なら、子供の `ChildView` から `RefreshAction` を受け取るだけで、必要な時にそれを `refresh?()` で呼び出せるので、一見最適解かもしれませんが、ちょっと致命的な問題点があります：タイトルに書いてある `UIViewRepresentable` 内部からは Preference の設定ができないのです。なので普通の `View` ならこの方法が一番お勧めしたいのですが、`UIViewRepresentable` からはこのまま使えないです。

### 本命

ではどうすればいいかと言うと、やはり微妙なところもありますが、個人的に一番お勧めしたい方法は `ChildView` の `init` 時に `RefreshAction` を渡す方法です：

```swift:RefreshAction.swift
struct RefreshAction: Equatable {
    private weak var uiView: ChildView.UIViewType?
    init(uiView: ChildView.UIViewType?) {
        self.uiView = uiView
    }
    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.uiView === rhs.uiView
    }
    func callAsFunction() {
        uiView?.refresh()
    }
}
```

```swift:ChildView.swift
struct ChildView: UIViewRepresentable {
    var refreshActionPassing: @MainActor (RefreshAction) -> Void
    func makeUIView(context: Context) -> SomeUIView {
        let uiView = SomeUIView()
        passRefreshAction(.init(uiView: uiView))
        return uiView
    }
    @MainActor
    func passRefreshAction(_ action: RefreshAction) {
        Task {
            refreshActionPassing(action)
        }
    }
}
```

```swift:ParentView.swift
struct ParentView: View {
    @State private var refresh: RefreshAction?
    var body: some View {
        HStack {
            ChildView(refreshActionPassing: {
                refresh = $0
            })
            Button("Refresh") {
                refresh?()
            }
        }
    }
}
```

この方法の最大のメリットは、`ParentView` から直接明示的に `refresh` を呼び出せながら、`refresh` の詳細実装に関心を持たずに済むところです。特にまだ SwiftUI に対応していない 3rd Party ライブラリーでまだ `UIView` バージョンのビューしか提供していないことが多いですが、これで SwiftUI からでも扱いやすくなったと思います。もちろん最初に紹介した `refreshingID` を利用した方法でも同じことができますが、やはり呼び出しが直感的ではないので、個人的にはあまり好みではないです。

とは言え、このような状態伝搬の仕方はやはり一般的ではないから、伝搬処理の関数に `Passing` 後置詞を入れるなど、できる限りの工夫も必要かと思います。

またこの方法で実装するとき、気をつけないといけないのは `ChildView` の `passRefreshAction(_:)` の実装です。見ての通りその中では別に非同期処理が全くないにも関わらず `Task` で処理を囲んでいます。なぜかというと、`makeUIView` の実装でこの `passRefreshAction` を呼び出すと、`refreshAction` が親に渡された後、そのまま `@State` に入れられると想定されますが、`makeUIView` はビューのレンダリング中に呼ばれる処理ですので、すなわちビューのレンダリング中に `@State` を更新することになります。その結果、SwiftUI ではお馴染みの `Modifying state during view update, this will cause undefined behavior` ワーニングが発生します。なので、ここで `Task` で囲むことによって、ビューのレンダリング後のサイクルで `@State` を更新しますから、このワーニングを回避します。

## まとめ

以上、4つの方法をご紹介してきましたが、状況を見て最適な設計を選んでいただけたら幸いです。