UIViewRepresentable を理解して SwiftUI の足りないところを UIKit で補う

## 概要

SwiftUI で開発していると、 UIKit で使っていた機能に対応するコンポーネントがなくて困るということがよく起こります。そんなときに SwiftUI の中で UIKit を使えるようにしてしまえばめちゃくちゃ便利じゃん...という素人の発想を実現してくれるのが UIViewRepresentable です。とくに SwiftUI があまり成熟していなかった iOS 13 をサポートしているアプリでは UIViewRepresentable に頼る場面が多くなるのではないでしょうか。

UIViewRepresentable の API は非常にシンプルなので既存の利用例をざっと見ただけでも使えてしまうのですが、一度ちゃんと理解しておこうと思って調べたのでまとめます。もしこの記事に修正・改善すべき点があればコメントで教えていただけると助かります。

検証環境は以下です。

- Xcode 13.2 Beta 2
- iOS 15.2

## 使い方

まずは UIViewRepresentable の基本的な使い方についてです。SwiftUI の中で UIView を使いたいと思ったときにやることは、

- UIView をラップする UIViewRepresentable を作り、そのメソッドとして `makeUIView` と `updateUIView` を実装する
- 作った UIViewRepresentable を SwiftUI の View の中で呼び出す

の２つだけです。

UIViewRepresentable を作る方から見ていきます。まずは冷静に UIViewRepresentable のシグネチャを眺めてみましょう。

- https://developer.apple.com/documentation/swiftui/uiviewrepresentable

上記のドキュメントから必要な情報のみ抜粋したものを以下に示します。

```swift
public protocol UIViewRepresentable : View where Self.Body == Never {
    // どの型の UIView をラップするかを表す
    associatedtype UIViewType: UIView

    // ラップする UIView のインスタンスを作って返す
    func makeUIView(context: Self.Context) -> Self.UIViewType
    // データの更新に応じてラップしている UIView を更新する
    func updateUIView(_ uiView: Self.UIViewType, context: Self.Context)
}
```

どの UIView をラップするかが `associatedtype` の `UIViewType` として定義されており、これが protocol のメソッドのシグネチャにも使われています。`UILabel` をラップしたいときには `UILabel` を、`UIScrollView` をラップしたいときには `UIScrollView` を `UIViewType` に指定します。

開発者側での実装が必須なメソッドは２つです。
まず、`makeUIView` にて、ラップしたい `UIViewType` のインスタンスを生成して返します。例えば `UILabel` や `UIScrollView` のイニシャライザを呼んで、必要な設定をしてからそれを `return` することになるでしょう。
`updateUIView` は UIViewRepresentable の状態が更新されるたびに呼ばれるメソッドです。ラップしている UIView のインスタンスが引数として渡されてくるので、更新されたデータや Environment を UIView に反映させるということを行います。

実際に使用例を見てみましょう。よくあるカウンター画面でカウントの表示に `UILabel` を使いたくなったときのことを考えます。念のため補足すると、この例は説明のための人工的なもので、同等のことを SwiftUI の `Text` で実現できるため UIViewRepresentable を使う必然性はありません。

Int 型の `count` を受け取って現在のカウントを表示する `UILabel` をラップする UIViewRepresentable を書いてみます。

```swift
struct CounterLabelRepresentable: UIViewRepresentable {
    // 受け取りたいデータをプロパティとして定義する
    let count: Int
    
    func makeUIView(context: Context) -> UILabel {
        UILabel()
    }
    
    func updateUIView(_ uiLabel: UILabel, context: Context) {
        uiLabel.text = "I am a UILabel and count is \(count)"
    }
}
```

UIViewRepresentable のシグネチャと見比べると、今回の `CounterLabelRepresentable` は associatedtype の `UIViewType` に `UILabel` を指定していることがわかると思います。`makeUIView` で `UILabel` のインスタンスを生成して返し、`updateUIView` で現在の `count` を表示に反映させています。
表示に必要なデータである `count` はプロパティとして定義します。ここで、プロパティは `@Binding` や `@State` にもできますが、この例は受け取った `count` を表示するだけの受動的な View なのでふつうのプロパティにしています。

続いて、この `CounterLabelRepresentable` を実際に使ってみましょう。

```swift
struct CounterView: View {
    @State var count: Int = 0
    
    var body: some View {
        VStack(alignment: .leading) {
            Button(action: { count += 1 }) {
                Text("Increment")
            }
            
            // 作った UIViewRepresentable を呼び出すだけ
            CounterLabelRepresentable(count: count)
                .frame(height: 44)
        }
        .padding()
    }
}
```

注目したいのは`CounterLabelRepresentable` が UIKit の `UILabel` をラップしているにも関わらず、呼び出し側からは単なる SwiftUI の View であるかのように見えていることです。もちろん modifier もつけることができ、ここでは `.frame` で高さを指定しています。
UIViewRepresentable の利用者側からはその実装が UIView であることを意識する必要がないというのはとてもよい設計だと思います。

この画面は以下のように動作します。

![counter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/ca0a38e5-5d11-fb96-85b7-11cbcb339007.gif)

## 各メソッドとライフサイクル

基本的な使い方がわかったところで、UIViewRepresentable の各メソッドについてもう少し詳しく調べてみます。UIViewRepresentable を理解する上での重要な要素に Coordinator がありますが、これについてはのちほど説明するのでここでは Coordinator が関わらないメソッドのみ見ることにします。

以下では UIViewRepresentable のライフサイクルという概念が出てきますが、これは UIViewRepresentable に対応する View が生成されてから消去されるまでのことを表していて、`UIViewRepresentable` の protocol に準拠している struct の寿命とは関係がないことに注意してください。基本的に View は生成されると非表示になるまで生き続けますが、その View を記述する struct は再描画のたびに作り直されます。
このあたりの SwiftUI の View のライフサイクルの話は難しくて自分が完全に理解できているか怪しいのですが、以下の WWDC の２つのセッションを見れば概要は掴めると思います。

- [Data Essentials in SwiftUI (WWDC 2020)](https://developer.apple.com/videos/play/wwdc2020/10040/)
- [Demystify SwiftUI](https://developer.apple.com/videos/play/wwdc2021/10022/)

### `func makeUIView`

`makeUIView` は UIViewRepresentable のライフサイクルの中で一度だけ最初に呼ばれ、ラップする対象の UIView を生成します。UIView のインスタンスを生成したら、一度だけ行えばよい、データに関わらず不変な設定処理などをここに書くのがよいでしょう。例えば、`UILabel` をラップする場合はフォントサイズやカラーの設定などが考えられます。また、AutoLayout を設定する必要がある場合もここでやることが多いでしょう。

### `func updateUIView`

`updateUIView` は `makeUIView` が呼ばれた直後に一度、その後は View の状態が更新されるたびに呼ばれます。その性質上、データを View に反映させる責任は `makeUIView` ではなく `updateUIView` にあります。もちろん変化することがないデータの反映や、データが変化する場合も初期データの反映だけは `makeUIView` で行うことが可能ですが、いずれにしても `makeUIView` の直後に `updateUIView` が呼ばれるのでデータの反映は `updateUIView` のみで行うのがわかりやすいと思います。

### `static func dismantleUIView`

`dismantleUIView` は UIViewRepresentable のライフサイクルの終わりに呼ばれ、もしなんらかの後処理が必要であればここで行います。subscription の解除や、この View が消えることをきっかけにアプリの他の画面で何かしたい場合の通知などが考えられます。
ちなみに、dismantle というのは取り外すとか分解するみたいな意味らしいです。

### 各メソッドの呼び出しタイミング

ここまでに上げたメソッドがいつ呼び出されるかを検証してみましょう。検証に使う例として、先ほどのカウンターを以下のように変えたものを用意します。いくつかの変更を同時に加えていますが、いずれも小さな変更です。

- `CounterLabelRepresentable` の各メソッドや親 View の `body` にデバッグ出力を追加
- 中身の UIView のライフサイクルも知りたいのでラップする UIView を `UILabel` から自前の `MyUILabel` に変更。機能は同じだが `init` / `deinit` 時にデバッグ出力を追加
- `UIViewRepresentable` の表示 / 非表示を切り替える `Toggle` を追加

```swift
struct CounterView: View {
    @State var count: Int = 0
    @State var showingLabel: Bool = true

    var body: some View {
        let _ = print("Parent \(#function)")

        VStack(alignment: .leading) {
            Toggle(isOn: $showingLabel) {
                Text("Showing Label")
            }

            Button(action: { count += 1 }) {
                Text("Increment")
            }
            
            if showingLabel {
                CounterLabelRepresentable(count: count)
                    .frame(height: 44)
            }
        }
        .padding()
    }
}

struct CounterLabelRepresentable: UIViewRepresentable {
    let count: Int
    
    init(count: Int) {
        print("CounterLabelRepresentable \(#function)")
        self.count = count
    }
    
    func makeUIView(context: Context) -> MyUILabel {
        print("Representable \(#function)")
        return MyUILabel()
    }
    
    func updateUIView(_ uiLabel: MyUILabel, context: Context) {
        print("CounterLabelRepresentable \(#function)")
        uiLabel.text = "I am a UILabel and count is \(count)"
    }
    
    static func dismantleUIView(_ uiLabel: MyUILabel, coordinator: ()) {
        print("CounterLabelRepresentable \(#function)")
    }
}

class MyUILabel: UILabel {
    override init(frame: CGRect) {
        print("UILabel \(#function)")
        super.init(frame: frame)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    deinit {
        print("UILabel \(#function)")
    }
}
```

動作は `Toggle` が加わったこと以外は同じです。

![toggled_counter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/56a23b52-412d-86d2-2324-a74bc0db702f.gif)

以下、いくつかの操作に対してどのメソッドが呼ばれるかをデバッグ出力から見ていきます。

#### 表示時

まず、初回の表示時のデバッグ出力を見てみます。

```
Parent body
CounterLabelRepresentable init(count:)
CounterLabelRepresentable makeUIView(context:)
UILabel init(frame:)
CounterLabelRepresentable updateUIView(_:context:)
```

親 View の `body` が呼ばれ、`body` に含まれる `CounterLabelRepresentable` struct の `init` が呼ばれています。その後、`makeUIView` が呼ばれ、実体の `UILabel` のインスタンスが生成されています。最後に、`updateUIView` が呼ばれていることがわかります。データ更新時だけでなく初回の表示時にも `updateUIView` が呼ばれることは `UIViewRepresentable` を使う上で知っておく必要があります。

#### 更新時

続いて、Increment ボタンをタップして `count` を変化させます。

```
Parent body
CounterLabelRepresentable init(count:)
CounterLabelRepresentable updateUIView(_:context:)
```

`count` が変化することにより親 View の `body` が再評価され、`updateUIView` が呼ばれて表示が変化します。ここで `init` により `CounterLabelRepresentable` の struct は初期化されていますが、その裏にある `UILabel` は作り直されず、既存のインスタンスが使いまわされていることがわかります。UIView のインスタンス生成は View の struct と比べてコストが高いため、パフォーマンスを落とさないようにインスタンスが使い回される設計になっているのだと思います。

#### 非表示時

続いて、`Toggle` をタップして `CounterLabelRepresentable` を非表示にしてみます。

```
Parent body
CounterLabelRepresentable updateUIView(_:context:)
CounterLabelRepresentable dismantleUIView(_:coordinator:)
UILabel deinit
```

非表示になったことにより `dismantleUIView` が呼ばれ、その後裏の `UILabel` も `deinit` されています。このあと再び `Toggle` をタップして `CounterLabelRepresentable` を表示すると、初回表示と同じように `CounterLabelRepresentable` と `UILabel` が生成されます。裏の UIView ができるだけ使いまわされると言っても、さすがに非表示 -> 表示するとインスタンスは作り直されるようです。また、意外だった点として非表示になる時も `updateUIView` が呼ばれるようです。

#### 親 View のみ再描画時

ここまでは `CounterLabelViewRepresentable` に関わる状態が変化したときの振る舞いを見てきましたが、親 View の body は再描画されるけど `CounterLabelViewRepresentable` が依存するデータには変化がないときにどのメソッドが呼ばれるのかも気になります。

`CounterLabelViewRepresentable` に関係ないデータとして、画面の背景色を親 View の状態として持たせて `Toggle` で更新できるようにしましょう。追加の差分のみを示します。

```swift
struct CounterView: View {
    @State var backgroundColored: Bool = false

    var body: some View {
        // ...
        VStack(alignment: .leading) {
            Toggle(isOn: $backgroundColored) {
                Text("Coloring Background")
            }
            
            // ...
        }
        // ...
        .background(backgroundColored ? Color.blue.opacity(0.2) : Color.clear)
    }
}
```

動作は以下のようになります。

![colored_counter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/45d55a99-f1fd-b598-1309-26fb4b9b402a.gif)

ここで、背景色の有無を切り替えたときのデバッグ出力は以下です。

```
Parent body
CounterLabelRepresentable init(count:)
```

`@State` が更新されたことによって親 View の `body` の再評価がされ、それに伴い `CounterLabelRepresentable` が `init` で初期化されています。ただし `count` を変化させたときと違って `updateUIView` が走らないことがわかります。この例では `CounterLabelRepresentable` が依存するデータが変化していないので再描画の必要はなく、パフォーマンスの観点からは望ましい振る舞いが実現されていると言えます。SwiftUI は各 View がどのデータに依存しているのかを把握し、データの更新が発生するとそのデータを使っている View のみを再描画するということだと思います。

## Environment

ここまでは UIViewRepresentable の表示に必要なデータを引数として渡す場合について見てきました。実は、通常の SwiftUI の View と同じように UIViewRepresentable は Environment からも値を受け取ることができます。

カウンターの例を少し書き変えて、親 View から `CounterLabelRepresentable` に `disabled` modifier をつけて、値を切り替えられるようにします。 `CounterLabelRpresentable` の側では、引数の `updateUIView` の引数の `context` の `environment.isEnabled` からその値が取得できるので、それに応じてテキストの色を変化させます。 `disabled` が `true` の時はテキストの色を薄くすることにしましょう。

```swift
struct CounterView: View {
    // ...
    @State var enabled: Bool = true

    var body: some View {
        VStack(alignment: .leading) {
            Toggle(isOn: $enabled) {
                Text("Enabling Label")
            }

            // ...

            if showingLabel {
                CounterLabelRepresentable(count: count)
                    .frame(height: 44)
                    .disabled(!enabled)
            }
        }
        // ...
    }
}

struct CounterLabelRepresentable: UIViewRepresentable {
    func updateUIView(_ uiLabel: MyUILabel, context: Context) {
        // ...
        uiLabel.textColor = context.environment.isEnabled ? .black : .gray
    }
}
```

動作させてみると、以下のように Environment の変化が表示に反映させていることがわかります。ここで、現在の Environment を UIViewRepresentable に反映するのも `makeUIView` ではなく `updateUIView` で行う必要があることに注意してください。

![disabled_counter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/4ba45427-7ef0-a02b-dd44-0727db3406cb.gif)

`disabled` を切り替えたときのデバッグ出力は以下で、Environment が変化すると引数の値が変わったときと同じようにメソッドが呼ばれていることがわかります。

```
Parent body
CounterLabelRepresentable init(count:)
CounterLabelRepresentable updateUIView(_:context:)
```

## `Coordinator`

ここまで見てきた `CounterLabelRepresentable` は SwiftUI の親 View から情報を渡されるだけの受動的なコンポーネントでしたが、UIViewRepresentable からなんらかのアクションを起こしたい場合は Coordinator という仕組みが必要になります。

簡単な例として、カウンター画面の Increment ボタンを UIButton で書き直す例を考えてみます。以下のようになるでしょう。

```swift
struct IncrementButtonRepresentable: UIViewRepresentable {
    let onTapped: () -> Void

    init(onTapped: @escaping () -> Void) {
        self.onTapped = onTapped
    }

    func makeUIView(context: Context) -> UIButton {
        let button = UIButton(type: .system)
        button.setTitle("I am a UIButton and Increment", for: .normal)

        // TODO: onTap を呼んで count を increment する

        return button
    }

    func updateUIView(_ uiButton: UIButton, context: Context) {}
}
```

コード中に TODO としてコメントしたように、現状のコードだと親 View から渡されたボタンタップ時のアクション `onTapped` を発火させる方法がありません。最初に思いつくのは以下のように UIViewRepresentable にアクションを定義してしまうことでしょう。しかし、このコードはコンパイルエラーになってしまいます。

```swift
struct IncrementButtonRepresentable: UIViewRepresentable {
    func makeUIView(context: Context) -> UIButton {
        // ...
        button.addTarget(self, action: #selector(didTap), for: .touchUpInside)
        // ...
    }

    // ...

    // ❌ @objc can only be used with members of classes, @objc protocols, and concrete extensions of classes
    @objc
    func didTap() {
        onTapped()
    }
}
```

コンパイルエラーの直接の原因は struct には `@objc` メソッドを定義できないことですが、仮に文法上許されていたとしても UIViewRepresentable の struct はいつ親 View の再描画により作り直されるかわからない一時的な存在なのでアクションの定義先としては不適切でしょう。

このこと考えると、ラップする UIView と同じライフサイクルを持ち、かつ `@objc` メソッドが定義可能な `class` にアクションを定義したいです。そんな都合のよいものないよな...と思いきやちゃんと用意されていて、それが Coordinator です。

Coordinator は UIViewRepresentable がラップする UIView のデリゲートとして働きます。このデリゲートというのは UIView のアクションに対応して何かするものというくらい意味で、`UIScrollViewDelegate` や `UISearchBarDelegate` のような Delegate と名のつく protocol であることもありますし、今回の例のように単に UIButton のアクションの定義先であることもあります。Delegate protocol に Coordinator を使う例としては、例えば以下の記事を参照ください。

https://www.hackingwithswift.com/books/ios-swiftui/using-coordinators-to-manage-swiftui-view-controllers

Coordinator を使う上でやることは以下です。

- Coordinator として使う class を定義する。UIViewRepresentable にネストさせて `Coordinator` という名前で定義することが多い
- UIViewRepresentable の `makeCoordinator` メソッドの中で Coordinator を生成して返す
- `makeUIView` / `updateUIView` メソッドの引数の `context.coordinator` で Coordinator が取得できるので Coordinator に行わせたい処理を設定する

`IncrementButtonRepresentable` を Coordinator を使うように修正すると、例えば以下のようになるでしょう。

```swift
struct CounterView: View {
    @State var count: Int = 0
    // ...

    var body: some View {
        // ...
        VStack(alignment: .leading) {
            // ...

            IncrementButtonRepresentable(onTapped: { count += 1 })
                .frame(height: 44)

            // ...
        }
        // ...
    }
}


struct IncrementButtonRepresentable: UIViewRepresentable {
    final class Coordinator {
        let onTapped: () -> Void
        
        init(onTapped: @escaping () -> Void) {
            self.onTapped = onTapped
        }
        
        @objc
        func didTap() {
            onTapped()
        }
    }
    
    let onTapped: () -> Void
    
    init(onTapped: @escaping () -> Void) {
        self.onTapped = onTapped
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(onTapped: onTapped)
    }
    
    func makeUIView(context: Context) -> UIButton {
        let button = UIButton(type: .system)
        button.setTitle("I am a UIButton and Increment", for: .normal)
        button.addTarget(context.coordinator, action: #selector(Coordinator.didTap), for: .touchUpInside)
        
        return button
    }
    
    func updateUIView(_ uiButton: UIButton, context: Context) {}
}
```

以下のようにきちんと動作するようになります。ちなみに、デバッグ出力を仕込むと、Coordinator は `IncrementButtonRepresentable` がラップする UIButton と同じタイミングで生成・消滅することがわかります。

![uibutton_counter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/f68d7662-84b1-26be-b2e8-aa927c441608.gif)

## まとめ

- UIViewRepresentable は SwiftUI の中で一部のみ UIView を使うためのしくみ
- `makeUIView` は初期化時に一度だけ、`updateUIView` は初期化時とその後の状態更新時に呼ばれる。データを表示に反映するのは `updateUIView` で行う
- 実体の UIView のインスタンスは状態更新のたびに作り直されるのではなく、非表示になるまで使い回される
- UIViewRepresentable からなんらかのアクションを起こしたい場合は Coordinator を使う

## 参考

- https://developer.apple.com/documentation/swiftui/uiviewrepresentable
- https://developer.apple.com/documentation/swiftui/uiviewrepresentablecontext
- https://www.hackingwithswift.com/books/ios-swiftui/using-coordinators-to-manage-swiftui-view-controllers
- https://swiftontap.com/UIViewRepresentable
- https://www.swiftbysundell.com/articles/swiftui-and-uikit-interoperability-part-1/