UICollectionViewの行セル、ヘッダー、フッター、またはUITableView内でSwiftUIビューを使用（iOS 16, UIHostingConfiguration）

iOS 16では、UIKitのコレクションビュー（UICollectionView）やテーブルビュー（UITableView）の中で、行セル（cell）、ヘッダー、フッターにSwiftUIビューを使用することができます。

![swiftui-collectionview-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/2cc3d42b-6860-58c0-4076-1ea736b88f5e.png)

これは新しい `UIHostingConfiguration` を使用することで実現できます。

この記事では、UICollectionViewの行コンテンツ、ヘッダービュー、フッタービューとして、
またUITableViewの行コンテンツとしてSwiftUIビューを使うことについて説明します。

# `UICollectionView` と `UITableView` の違い

`UITableView` が簡単にテーブルを表示できるのに対し、 `UICollectionView` はカスタマイズのための機能が充実しています。また、`UICollectionView`のインターフェースは `UITableView` に比べてより現代的です (角丸やパディングが自動で行われます)。

`UICollectionView` はリスト配置を使用すると、アイテムを縦に並べて表示するテーブルビューとして使用することができます。

| UICollectionView + SwiftUI | UITableView + SwiftUI |
|---|---|
| ![swiftui-collectionview-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/2cc3d42b-6860-58c0-4076-1ea736b88f5e.png) | ![swifui-tableview-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/0d93e619-387b-33a4-18f5-c87a00c18db7.png) |

# サンプル使用

この記事では、クラウドサーバーの設定を表示するテーブルを構築する例を使用します。各セルには基本的な情報が含まれ、UIには星印ボタンが表示されます（ユーザーはクリックすることで項目のスターを追加または削除できます）。

```swift
struct ServerConfiguration {
    var serverNickname: String
    var serverRegion: String
    var serverHardwareType: String
    var numberOfCPUCores: Int
    var gbOfMemory: Int
    var networkMaxSpeedGB: Int?
    var isServerStarred: Bool // <-- この変数は、テーブルビューの行UIから直接変更することができます。
}
```

以下はテスト用のデータです。

```swift
enum DemoData {
    static let demoServers: [ServerConfiguration] = [
        .init(serverNickname: "Kitsu",
              serverRegion: "ap-northeast-1",
              serverHardwareType: "m6g.medium",
              numberOfCPUCores: 1,
              gbOfMemory: 4,
              networkMaxSpeedGB: 10,
              isServerStarred: true
             ),
        .init(serverNickname: "Neko",
              serverRegion: "ap-northeast-3",
              serverHardwareType: "t2.micro",
              numberOfCPUCores: 1,
              gbOfMemory: 1,
              isServerStarred: false
             )
    ]
}
```

# `UICollectionView` の場合

## スタート点

`UICollectionViewController` を使用して作成した新しいビューコントローラーからスタートします
`viewDidLoad` 関数の中で、 `UICollectionViewCompositionalLayout.list` を使用して、リスト（またはテーブル）のような要素を表示するコレクションビューを設定します。

```diff_swift
class ViewController: UICollectionViewController {
    
    let servers = DemoData.demoServers
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Set the collection view to list layout
+        var listLayoutConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
+        let listLayout = UICollectionViewCompositionalLayout.list(using: listLayoutConfig)
+        self.collectionView.collectionViewLayout = listLayout
    }
    
    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return servers.count
    }
    
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        // ...
    }

}
```

## データ項目を `ObservableObject` としてマークする

アイテム（テーブルに表示されるデータを含むオブジェクト）を `ObservableObject` プロトコルに適合させる必要があります。

そして、変更可能な変数に `@Published` 修飾子を追加する必要があります。

そうすることで、SwiftUIのビューはアイテムに保存されたデータを変更できるようになります。

たとえば、ユーザーがサーバーにスターをつけることができる場合、スイッチをSwiftUIビュー内に設置することができます。

変数を `@Published` としてマークすることで、SwiftUI ビューは変数の値を更新できます; 
また、変数が他の場所で変更されたとき (たとえば UICollectionView のボタンによって)、SwiftUI ビューは自動的に更新します (テーブル全体もしくは行を手動で再ロードする必要はありません)。

```diff_swift
- struct ServerConfiguration {
+ class ServerConfiguration: ObservableObject {
    var serverNickname: String
    var serverRegion: String
    var serverHardwareType: String
    var numberOfCPUCores: Int
    var gbOfMemory: Int
    var networkMaxSpeedGB: Int?
-   var isServerStarred: Bool
+   @Published var isServerStarred: Bool
    
+    init(serverNickname: String, serverRegion: String, serverHardwareType: String, numberOfCPUCores: Int, gbOfMemory: Int, networkMaxSpeedGB: Int? = nil, isServerStarred: Bool) {
+        self.serverNickname = serverNickname
+        self.serverRegion = serverRegion
+        self.serverHardwareType = serverHardwareType
+        self.numberOfCPUCores = numberOfCPUCores
+        self.gbOfMemory = gbOfMemory
+        self.networkMaxSpeedGB = networkMaxSpeedGB
+        self.isServerStarred = isServerStarred
+    }
}
```

## SwiftUIビューをデザインする

行セルcellとして使用されるSwiftUIビューを設計する必要があります。

そのSwiftUIビューはデータアイテム(この場合、`ServerConfiguration`オブジェクト)を入力として受け取る必要があります。

```swift
struct ServerCollectionViewCell: View {
    @ObservedObject var item: ServerConfiguration
    var body: some View {
        HStack {
            // 星マークボタン
            Button {
                item.isServerStarred.toggle()
            } label: {
                Image(systemName: item.isServerStarred ? "star.fill" : "star")
            }
            // サーバーのニックネーム
            Text(item.serverNickname)
        }
    }
}
```

ObservedObject`を使用しているため、SwiftUI のビューが `@Published` でマークされた変数からの変更を受信し、ビューをリロードできます。

`@Published` でマークされた変数については、`$` シンボル (`$isServerStarred` など) を使用して、変数のバインディング（＠Binding) を取得することもできます。

## 行の表示のためのレジスタを準備する (`CellRegistration`)

行の表示にSwiftUIビューを使いたいことをテーブルビューに知らせるために、レジスタを設定する必要があります。

まず、コレクションビューまたはテーブルビューファイルで `import SwiftUI` します。
次に、提供されたアイテムに基づいたSwiftUIビューを返すレジスタを追加します。
レジスタは、呼び出されたときに値を計算する変数として追加されます。

```diff_swift
class ViewController: UICollectionViewController {
    
    let servers = DemoData.demoServers
    
+    // Configuration for using SwiftUI for cell
+    private var swiftUICellViewRegister: UICollectionView.CellRegistration<UICollectionViewListCell, ServerConfiguration> = {
+        .init { cell, indexPath, item in
+            let hostingConfiguration = UIHostingConfiguration {
+                // Provide the SwiftUI view components here
+                ServerCollectionViewCell(item: item)
+            }
+            // 上記のビューを行セルに設定する
+            cell.contentConfiguration = hostingConfiguration
+        }
+    }()

    // ...

}
```

このレジスタの中で、行のcellを新しく設計された SwiftUI のビュー `ServerCollectionViewCell` に設定します。

## コレクションビューの行cell表示のためにレジスタを適用する

`cellForItemAt` デリゲート関数の中で、コレクションビューの行cellを生成するために上記のレジスタを使用します。

```diff_swift
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let serverInfo = servers[indexPath.row]
+    let cell = collectionView.dequeueConfiguredReusableCell(using: swiftUICellViewRegister, for: indexPath, item: serverInfo)
    return cell
}
```

## コレクションビューのコード

以下は、SwiftUIビューを行cellにしたコレクションビューのコードです。

```swift
class ViewController: UICollectionViewController {
    
    let servers = DemoData.demoServers
    
    // Configuration for using SwiftUI for cell
    private var swiftUICellViewRegister: UICollectionView.CellRegistration<UICollectionViewListCell, ServerConfiguration> = {
        .init { cell, indexPath, item in
            let hostingConfiguration = UIHostingConfiguration {
                // Provide the SwiftUI view components here
                ServerCollectionViewCell(item: item)
            }
            // Set the above view to the cell
            cell.contentConfiguration = hostingConfiguration
        }
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Set the collection view to list layout
        var listLayoutConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        let listLayout = UICollectionViewCompositionalLayout.list(using: listLayoutConfig)
        self.collectionView.collectionViewLayout = listLayout
    }
    
    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return servers.count
    }
    
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let serverInfo = servers[indexPath.row]
        let cell = collectionView.dequeueConfiguredReusableCell(using: swiftUICellViewRegister, for: indexPath, item: serverInfo)
        return cell
    }

}
```

シミュレータでコードを実行し、
SwiftUIビューが行cellの表示として使われているのがわかるはずです。

また、各セルの星印のアイコンをクリックすると、星印としてマークすることができます。
`ObservableObject` と `@Published` 修飾子を使用しているので、
SwiftUI から行われた変更は変数（UIKitコード内の `servers` 配列）に行われることになります。

## ヘッダーとフッターの表示を追加する

### ヘッダーとフッターの有効化

まず、ヘッダーとフッターのモードを `.supplementary` に設定します。
これは、リスト形式の表示を設定するときに、`viewDidLoad` 関数内で行うことができます。


```diff_swift
class ViewController: UICollectionViewController {

    // ...

    override func viewDidLoad() {
        super.viewDidLoad()
        // Set the collection view to list layout
        var listLayoutConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
+        listLayoutConfig.headerMode = .supplementary
+        listLayoutConfig.footerMode = .supplementary
        let listLayout = UICollectionViewCompositionalLayout.list(using: listLayoutConfig)
        self.collectionView.collectionViewLayout = listLayout
        // ...
    }

    // ...

}
```

### レジスタを用意する（`SupplementaryRegistration`）。

上記で各行cellにレジスタを用意したのと同様です。
ヘッダーまたはフッターのレジスタを変数として提供します。
例えば、ここではSwiftUIのテキストを含むフッターを追加しています。

```diff_swift
class ViewController: UICollectionViewController {
    
    let servers = DemoData.demoServers //...
    
    private var swiftUICellViewRegister //...
    
+    // フッタービューにSwiftUIを使用するための設定
+    private var swiftUIFooterViewRegister: UICollectionView.SupplementaryRegistration<UICollectionViewCell> = .init(elementKind: UICollectionView.elementKindSectionFooter) {
+        (footer, elementKind, indexPath) in
+        // `UIHostingConfiguration` を使用してフッターの内容を定義する。
+        footer.contentConfiguration = UIHostingConfiguration {
+            Text("Thanks for reading this article!")
+        }
+    }

    // ...

}
```

注意: このコードは計算された変数の中にあるからですから、ここでは `self` を使うことはできません。
`self` の使用については後で説明します。

### フッターの適用

ビューにフッターを適用するには、`viewForSupplementaryElementOfKind` デリゲート関数を使用します。

```diff_swift
class ViewController: UICollectionViewController {
    
    // ...

    override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        guard let swiftUIHeaderViewRegister else {
            return .init()
        }
        switch kind {
+        case UICollectionView.elementKindSectionFooter:
+            let footer = collectionView.dequeueConfiguredReusableSupplementary(
+                using: swiftUIFooterViewRegister,
+                for: indexPath
+            )
+            return footer
        default:
            return .init()
        }
    }

    // ...

}
```

### `self` にアクセスできるヘッダを追加する

例えば、ユーザーが全てのアイテムにスターを付けるためのボタンを用意したいとします。

まず、以下の変数を追加します。

```diff_swift
class ViewController: UICollectionViewController {
    
    let servers = DemoData.demoServers

 +   private var swiftUIHeaderViewRegister: UICollectionView.SupplementaryRegistration<UICollectionViewCell>?

    // ...

}
```

そして、`viewDidLoad` 関数内で、このレジスタを初期化することができます。

```swift
override func viewDidLoad() {
    // ...

+    // ヘッダーコンフィギュレーションを設定する
+    // ここでは、`self` を使用できるように設定しています
+    self.swiftUIHeaderViewRegister = .init(elementKind: UICollectionView.elementKindSectionHeader) { [unowned self]
+       (header, elementKind, indexPath) in
+       // Define header content using `UIHostingConfiguration`
+       header.contentConfiguration = UIHostingConfiguration {
+           HStack {
+               Button("Mark all as starred") {
+                   self.markAllAsStarred()
+               }
+               .buttonStyle(.borderedProminent)
+               Button("Default") {
+                   self.markAllAsNotStarred()
+               }
+               .buttonStyle(.borderedProminent)
+           }
+       }
+   }
}
```

上記のコードでは、コードが呼び出されたときにコレクションビューコントローラが既に初期化されているので、 `[unowned self]` を使用してこのコントローラ内のデータや関数にアクセスすることができます。

さて、ヘッダを適用するために `viewForSupplementaryElementOfKind` 関数を変更します。

```diff_swift
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
    guard let swiftUIHeaderViewRegister else {
        return .init()
    }
    switch kind {
+    case UICollectionView.elementKindSectionHeader:
+        let header = collectionView.dequeueConfiguredReusableSupplementary(
+            using: swiftUIHeaderViewRegister,
+            for: indexPath
+        )
+        return header
    case UICollectionView.elementKindSectionFooter:
        let footer = collectionView.dequeueConfiguredReusableSupplementary(
            using: swiftUIFooterViewRegister,
            for: indexPath
        )
        return footer
    default:
        return .init()
    }
}
```

`ObservableObject` と `@Published` 修飾子を使ったので、UIKitからの変更はデータ配列（サーバー）に適用され、SwiftUIビューは自動的に再読み込みされます。

# UITableViewの場合

## デフォルトのセル識別子をマークする

最初に、UITableView のために UIStoryBoard または xib ファイルに空のセルテンプレートを作成します。

ストーリーボードまたはXIBの空のセルの識別子を `cell` に変更する。

![スクリーンショット 2022-09-29 16.28.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/7ee1e660-eb42-e4e0-1447-891ce47c5b3c.png)

次に、`cellForRowAt` デリゲート関数の中で、まず、セルオブジェクトを取得します。

```swift
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
    let item = servers[indexPath.row]
}
```

それから、 `contentConfiguration` を使って、SwiftUIビューを表示するセルを設定します。

```diff_swift
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
    let item = servers[indexPath.row]
+    // Apply SwiftUI view as the content of the cell
+    cell.contentConfiguration = UIHostingConfiguration {
+        ServerCollectionViewCell(item: item)
+    }
    return cell
}
```

UITableViewでSwiftUIのセルを使うための完成したコードは次のとおりです。

```swift
class DemoTableViewController: UITableViewController {
    
    let servers = DemoData.demoServers
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return servers.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        let item = servers[indexPath.row]
        // Apply SwiftUI view as the content of the cell
        cell.contentConfiguration = UIHostingConfiguration {
            ServerCollectionViewCell(item: item)
        }
        return cell
    }
    
}
```

テーブルビューのヘッダーとフッターをSwiftUIビューに設定するには、`UIHostingController`を使用することができます。

```swift
+struct HelloWorldHeaderView: View {
+    var body: some View {
+        Text("Hello world!")
+    }
+}

class DemoTableViewController: UITableViewController {
    // ...
    
    override func viewDidLoad() {
        super.viewDidLoad()
+        tableView.tableHeaderView = UIHostingController(rootView: HelloWorldHeaderView()).view
    }

   // ...
}
```


---


お読みいただきありがとうございました。

[☺️ Twitter @MszPro](https://twitter.com/MszPro)
[🐘 Mastodon @me@mszpro.com](https://sns.mszpro.com/@me)

<img width="150" src="https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png">

---

:sunny: 私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます：
<img width="150" src="<https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png>">
 ![writing-quickly_emoji_400.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/640938e9-c121-8152-b8c4-8ab63124afcc.png)


Written by MszPro~

---

# 関連記事

・[iOSアプリで画面上のQRコード部分のみ輝度を上げ](https://qiita.com/MaShunzhe/items/5dfb4dbfca2a25cbaddf)

・[UICollectionViewの行セル、ヘッダー、フッター、またはUITableView内でSwiftUIビューを使用](https://qiita.com/MaShunzhe/items/3d98fa897aa1bcffc74b)


・[iPhone 14 ProのDynamic Islandにウィジェットを追加し、Live Activitiesを開始する](https://qiita.com/MaShunzhe/items/6cc8d9d204f40cbee19c)


・[iOS 16：秘密値の保存、FaceID認証に基づく個人情報の表示/非表示（LARight）](https://qiita.com/MaShunzhe/items/07c1e65277a5008106db)


・[iOS16 MapKitの新機能 : 地図から場所を選ぶ、通りを見回す、検索補完](https://qiita.com/MaShunzhe/items/693e6302876707d5684d)


・[SwiftUIアプリでバックグラウンドタスクの実行(ネットワーク、プッシュ通知) (BackgroundTasks, URLSession)](https://qiita.com/MaShunzhe/items/4e0d1032748fe4f398de)


・[WWDC22、iOS16：iOSアプリに画像からテキストを選択する機能を追加（VisionKit）](https://qiita.com/MaShunzhe/items/ed3985d05975bf35292d)


・[WWDC22、iOS16：数行のコードで作成できるSwiftUIの新機能（26本）](https://qiita.com/MaShunzhe/items/0ed36d8a6de7c56b1d34)


・[WWDC22、iOS 16：SwiftUIでChartsフレームワークを使ってチャートを作成する](https://qiita.com/MaShunzhe/items/cae62af8b07f4f474fd8)


・[WWDC22, iOS 16: WeatherKitで気象データを取得](https://qiita.com/MaShunzhe/items/dce45ccdd05c1f70cf54)


・[WWDC 2022の基調講演のまとめ記事](https://qiita.com/MaShunzhe/items/037580d2ed51a5a6e9d9)

---

---

# Github Repository

https://github.com/mszpro/UITableView_withSwiftUI_demo