Delegateを伴うUIKit実装のカスタムビューをSwiftUI上で使う

# はじめに
この記事は  [Goodpatch Advent Calendar 2021](https://qiita.com/advent-calendar/2021/goodpatch)  14日目の記事です。

SwiftUI が登場してはや3年が過ぎました。年々 Apple からは WWDC の場を通して大幅な機能拡充が発表され、そろそろプロダクト開発への利用にも応えうる水準に達してきたように感じています。

とはいえ、少し凝った独自UIの開発をしようとすると、宣言的な記述で実装する SwiftUI の枠組みだけでは実装が難しかったり、できたとしても無用に複雑化してしまうケースに陥るため、結局 UIKit に頼らざるを得なかった、という場面は続きそうです。

今回は、そんな UIKit ベースの独自UI実装を、SwiftUI に組み込む方法をまとめてみました。表示データの提供やイベントハンドリングは、UIKit ベースのビュー開発では一般的な DataSource／Delegate パターンで実装した事例をもとにしています。

# 今回のサンプル
[一昨年の Advent Calendar](https://qiita.com/ttgp/items/5bcaaa7b5193f83877b5) で、ちょうど UICollectionViewLayout をカスタムした、[レンガ状の CollectionView を実装](https://gist.github.com/p0dee/03e81df4b7dd6e7994d295686ca29d32)したので、これをSwiftUIで表示できるようにします。

<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543004/3b1ec552-cc0e-5dc9-32e6-5b09d17fd1fb.png width=500>

```swift
// MARK: - BrickCollectionView Protocols

protocol BrickViewDataSource: AnyObject {
    func numberOfItems(_ brickView: BrickCollectionView) -> Int
    func brickView(_ brickView: BrickCollectionView, itemAt index: Int) -> String
}

protocol BrickViewDelegate: AnyObject {
    func brickViewDidTapItem(_ brickView: BrickCollectionView, item: String)
}

// MARK: - BrickCollectionView

final class BrickCollectionView: UIView {
    
    private var collectionView: UICollectionView!
    
    weak var dataSource: BrickViewDataSource?
    weak var delegate: BrickViewDelegate?
    
    func reloadData() {
        collectionView.reloadData()
    }
    
}

// MARK: - UICollectionViewDataSource

extension BrickCollectionView: UICollectionViewDataSource {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return dataSource?.numberOfItems(self) ?? 0
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: BrickCollectionView.Cell.self), for: indexPath) as! Cell
        cell.contentView.backgroundColor = .white
        cell.label.text = item(for: indexPath)
        return cell
    }
    
}

// MARK: - UICollectionViewDelegate

extension BrickCollectionView: UICollectionViewDelegate {
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        delegate?.brickViewDidTapItem(self, item: item(for: indexPath))
    }
    
}

```

# SwiftUI で活用するための実装STEP

## 1. `UIViewRepresentable` に準拠した SwiftUI wrapper を定義する
まず初めに、対象の UIKit ビューの SwiftUI ラッパー表現である、 `UIViewRepresentable` を定義します。このプロトコルに準拠するには、Coordinator の定義と、以下のfunctionを実装する必要があります。

```swift
struct BrickView: UIViewRepresentable {

    // Coordinator の生成処理
    func makeCoordinator() -> Coordinator {
        Coordinator()
    }

    // ビュー生成の初回のみ呼びだされる処理
    func makeUIView(context: Context) -> BrickCollectionView {
        let ret = BrickCollectionView()
        // TODO: 実装
        return ret
    }

    // 表示値の変更に伴ってビューの表示を更新する際にシステムから呼びだされる処理
    func updateUIView(_: BrickCollectionView, context: Context) {
        // TODO: 実装
    }

    // UIKit ビューと SwiftUI 間の変化を中継する Coordinator
    class Coordinator: NSObject {
        // TODO: 実装
    }
}
```

## 2. SwiftUI wrapper に必要なプロパティを定義する

1. このUI上で外部から動的に与えられ、表示に連動するデータを `@Binding` として定義します
2. delegate 経由でのイベントハンドリングを SwiftUI 側にも提供するために、外部から指定されたコールバックを保持できるようにします

いずれも、イニシャライザや View Modifier 経由で指定し、外部から直接触ることはないため、`private` として十分です。

```swift
struct BrickView: UIViewRepresentable {

    @Binding private var items: [String]

    private var onSelectItemCallback: ((String) -> Void)?

    ...

```

## 3. Coordinator を定義する

1. Coordinator を実装
	1. Coodinatorには、今回実装しているUIViewRepresentable準拠のViewインスタンスをもとに初期化する設計とします
	2. ` @Binding`として定義した、動的に変化するデータもプロパティとして定義します
2. `makeCoordinator` を実装
	1. 上記にあわせて、Coordinatorの生成時に自身をパラメタに渡すようにします

```swift
struct BrickView: UIViewRepresentable {

    ...

    // 2. `makeCoordinator` を実装
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    ...

    // 1. Coordinator を実装
    class Coordinator: NSObject, BrickViewDataSource, BrickViewDelegate {
        var parent: BrickView
        var items: [String] = []

        var onSelectItemCallback: ((String) -> Void)?

        init(_ brickView: BrickView) {
            self.parent = brickView
        }
    }
```

## 4. Coordinator と DataSource/Delegate を定義する

Coordinator を、UIKit 側で定義した DataSource や Delegate に準拠させます。

```swift
    class Coordinator: NSObject, BrickViewDataSource, BrickViewDelegate {

        ...
  
        func numberOfItems(_ brickView: BrickCollectionView) -> Int {
            return items.count
        }
        
        func brickView(_ brickView: BrickCollectionView, itemAt index: Int) -> String {
            guard index < items.count else { return "" }
            return items[index]
        }
        
        func brickViewDidTapItem(_ brickView: BrickCollectionView, item: String) {
            onSelectItemCallback?(item)
        }
    }
```

## 5.　カスタム View の初期化/更新処理を実装

1. `makeUIView(context:)` を実装
   - UI生成の初回一度のみ呼び出される処理です
   - 生成したビューの `dataSource` `delefate` に、 `context` から得られる `coordinator` を指定します
2. `updateUIView(_:context:)` を実装
   - UIの更新時にシステムから都度呼び出される処理です
   - そのため、データに更新があるごとに、Coordinator のプロパティにも随時反映します

```swift
struct BrickView: UIViewRepresentable {
    ...

    func makeUIView(context: Context) -> BrickCollectionView {
        let ret = BrickCollectionView()
        ret.delegate = context.coordinator
        ret.dataSource = context.coordinator
        return ret
    }

    func updateUIView(_ brickView: BrickCollectionView, context: Context) {
        context.coordinator.items = items
        context.coordinator.onSelectItemCallback = onSelectItemCallback
        brickView.reloadData()
    }

    ...
```

## 6. Delegate の処理を SwiftUI 側に露出する View Modifier を実装する
- コールバックを指定するView Modifier を定義し、delegate 経由で伝播されるイベントハンドリングを、SwiftUI 側から指定できるようにします

```swift
struct BrickView: UIViewRepresentable {

    ...

    /// アイテムが選択された際のコールバックを指定します
    func onSelectItem(callback: @escaping (String) -> Void) -> Self {
        var ret = self
        ret.onSelectItemCallback = callback
        return ret
    }

    ...
```

## 7. SwiftUI側で利用する
こうした手順により生成したビューは、以下のように UIKit のラッパーであることを意識せずに利用することができます。

```swift
struct ContentView: View {
    
    @State var items: [String] = []
    @State var newItem: String = ""
    
    var body: some View {
        BrickView(items: $items)
           .onSelectItem { selected in
               print("selected: \(selected)")
           }
    }
}
```

# おわりに
Appleの提供するSwiftUIチュートリアルにも、この一部の実装ステップは解説されていますが、独自のカスタムビュー実装については試行錯誤した面があったので、手順化することで類似実装時の迷いをなくせるだろうと記事にしてみました。

実際に、単純な App 例として実装してみましたので、ご参考ください。

[p0dee / UIKit-SwiftUInize-Sample](https://github.com/p0dee/UIKit-SwiftUInize-Sample)

# 参考

[SwiftUI Tutorials / Interfacing with UIKit](https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit)