【SwiftUI】InsetGroupedListStyle()を使って、開閉可能なListを作りたい

## はじめに

Listには色々種類がありますが、中でも`InsetGroupedListStyle()`が気に入っています。
他にどんなUIのListがあるのかは、「[【SwiftUI】Listの使い方](https://capibara1969.com/2266/#toc10)」とかを確認ください。

SwiftUIで実装する上でいつも参考にさせていただいてます。

今回は、UIは`InsetGroupedListStyle()`なのですが、`SidebarListStyle()`のように開閉できるListを実装したかったので、それを紹介します。

`SidebarListStyle()`をそのまま使用するとセルの右にchevronが存在すると思うのですが、そのアイコンや挙動もカスタマイズしたかったので、結局`InsetGroupedListStyle()`を使用した実装になりました。

## 成果物

以下ができます。

- Sectionを押下した時に開閉する
- 開いているときと閉まっている時で、表示するアイコン（矢印）を変更する
- 開いているときと閉まっている時で、Section下の（コード上だとSectionの中になりますが）線の表示を切り替える

動画で見たい場合は、以下のGithubリポジトリで公開しているので、そのREADMEを確認ください。

https://github.com/kamimi01/VariableListForSwiftUI

![Simulator Screen Shot - iPhone 12 - 2021-09-04 at 16.54.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/4e09218f-e583-6a07-11f5-d864053c6fb0.png)


## 実装

```swift
struct ContentView: View {
    // テスト用データ（APIから取得したデータを格納したり、、今回は辞書を使っているので順番が保証されないのに注意）
    private let testData = ["食べ物": ["明太子パスタ", "ショートケーキ", "プロシュート", "パウンドケーキ"], "飲み物": ["コーラ", "ルイボスティー"]]
    
    // 選択されたSectionがどれかを格納しておくためのSet
    @State private var selection: Set<String> = []

    var body: some View {
        let keys: [String] = testData.map { $0.key }
        let values: [[String]] = keys.compactMap { testData[$0] }
        
        VStack {
            List {
                ForEach(keys.indices) { keyIndex in
                    Section(header: sectionHeader(title: keys[keyIndex], isExpanded: self.selection.contains(keys[keyIndex]))) {
                        
                        if self.selection.contains(keys[keyIndex]) {
                            ForEach(values[keyIndex].indices) { valueIndex in
                                Text(values[keyIndex][valueIndex])
                                    .foregroundColor(Color.black)
                            }
                        }
                    }
                }
                
            }
            .listStyle(InsetGroupedListStyle())
        }
    }
}

extension ContentView {
    private func sectionHeader(title: String, isExpanded: Bool) -> some View {
        Button(action: {self.selectDeselect(selectedSection: title)}) {
            VStack {
                HStack {
                    Text(title)
                    
                    Spacer()
                    
                    Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
                }
                
                if !isExpanded {
                    Divider()
                } else {
                    Divider()
                        .frame(height: 0)
                }
            }
        }
        .foregroundColor(Color.gray)
    }
    
    // Sectionが押下された時に、Setの中身を変更するメソッド
    private func selectDeselect(selectedSection: String) {
        if selection.contains(selectedSection) {
            selection.remove(selectedSection)
        } else {
            selection.insert(selectedSection)
        }
    }
}
```

コメントにも書いてますが、今回は表示するデータの格納する型が、Dictionaryなので**順番は保証されていない**です。
順番を保証したい場合は、別のデータ型を使用しないといけないですが、今回はサンプルなのでご了承。。

Set型を準備して、そこに選択されたSectionの文字列を保持しています。
今回は「食べ物」「飲み物」とかが入ってますが、多分IDとかもっと一意っぽい値の方が良いとは思います。

## 参考にした記事

- [[SwiftUI] [String: [String]]をセクションで分けて表示する](https://qiita.com/noppefoxwolf/items/0540311d6a5e84264b70)
- [Expand and Collapse List Rows with Animation in SwiftUI](https://www.vadimbulavin.com/expand-and-collapse-list-with-animation-in-swiftui/)

## 終わりに

まだまだSwiftUI勉強中の身なので、実は`SidebarListStyle()`を使用してもっと上手くできるのかもしれないです。より良い方法をご存知の方、ぜひコメントで教えてください！ :blush: