6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-09-29

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

swiftui-collectionview-2.png

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

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

UICollectionViewUITableView の違い

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

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

UICollectionView + SwiftUI UITableView + SwiftUI
swiftui-collectionview-2.png swifui-tableview-1.png

サンプル使用

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

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

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

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 を使用して、リスト(またはテーブル)のような要素を表示するコレクションビューを設定します。

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 ビューは自動的に更新します (テーブル全体もしくは行を手動で再ロードする必要はありません)。

- 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オブジェクト)を入力として受け取る必要があります。

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ビューを返すレジスタを追加します。
レジスタは、呼び出されたときに値を計算する変数として追加されます。

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を生成するために上記のレジスタを使用します。

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にしたコレクションビューのコードです。

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 関数内で行うことができます。

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のテキストを含むフッターを追加しています。

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 デリゲート関数を使用します。

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 にアクセスできるヘッダを追加する

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

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

class ViewController: UICollectionViewController {
    
    let servers = DemoData.demoServers

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

    // ...

}

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

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 関数を変更します。

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

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

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

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

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のセルを使うための完成したコードは次のとおりです。

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を使用することができます。

+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
🐘 Mastodon @me@mszpro.com


:sunny: 私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます:

writing-quickly_emoji_400.png

Written by MszPro~


関連記事

iOSアプリで画面上のQRコード部分のみ輝度を上げ

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

iPhone 14 ProのDynamic Islandにウィジェットを追加し、Live Activitiesを開始する

iOS 16:秘密値の保存、FaceID認証に基づく個人情報の表示/非表示(LARight)

iOS16 MapKitの新機能 : 地図から場所を選ぶ、通りを見回す、検索補完

SwiftUIアプリでバックグラウンドタスクの実行(ネットワーク、プッシュ通知) (BackgroundTasks, URLSession)

WWDC22、iOS16:iOSアプリに画像からテキストを選択する機能を追加(VisionKit)

WWDC22、iOS16:数行のコードで作成できるSwiftUIの新機能(26本)

WWDC22、iOS 16:SwiftUIでChartsフレームワークを使ってチャートを作成する

WWDC22, iOS 16: WeatherKitで気象データを取得

WWDC 2022の基調講演のまとめ記事



Github Repository

6
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?