【UIKit】モダンなUICollectionViewを知りたい

# はじめに
公式で公開されている`Implementing Modern Collection Views`の[サンプルコード](https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views)を見てできることの多さに驚き、最新のコレクションビューのAPIについて調べてみることにしました。

## コレクションビューの進化
👇こちらのWWDC20のプレゼンで詳しく説明されていました

https://developer.apple.com/videos/play/wwdc2020/10097

### iOS12まで（私の理解していたコレクションビュー）
![スクリーンショット 2022-02-28 16.10.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/ac4e534b-a8b6-7e78-dfb8-5beb6720dc15.png)

### iOS13から`Data`と`Layout`が変わってこうなった
![スクリーンショット 2022-02-28 16.11.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/215a4b9a-5ec1-b85c-1421-1559b1f23309.png)
`Diffable Data Source`について👇

https://developer.apple.com/videos/play/wwdc2019/220

`Compositional Layout`について👇

https://developer.apple.com/videos/play/wwdc2019/215




### iOS14からはさらに進化した
![スクリーンショット 2022-02-28 16.12.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/d1e7f6b1-1c17-2787-82fa-9f313b68e7b6.png)

`Section Snapshots`について👇

https://developer.apple.com/videos/play/wwdc2020/10045

`List Configuration`について👇

https://developer.apple.com/videos/play/wwdc2020/10026

`List Cell`と`View Configuration`について👇

https://developer.apple.com/videos/play/wwdc2020/10027

# サンプルコードを読んでみる

まずはサンプルアプリを表示します
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/744184e0-8d5c-d0fd-c671-d814e5eb81f8.png">
**Modern Collection Views**
- Compositional Layout
- Diffable Data Source
- Lists
- Outlines
- Cell Configuratons
の構成となっていますが、初期表示画面を見ていくだけでも理解が深まると思いましたので、初期表示画面のサンプルコードを見ていきます

### 初期表示画面のViewController
`OutlineViewController`となっていますが、これは初期画面（テーブルビューの見た目のコレクションビュー）のもので、項目の`Outlines`とは別です💡

```swift:OutlineViewController
class OutlineViewController: UIViewController {
    // ※1
    enum Section {
        case main
    }
    // ※1
    class OutlineItem: Hashable {
        let title: String
        let subitems: [OutlineItem]
        let outlineViewController: UIViewController.Type?

        init(title: String,
             viewController: UIViewController.Type? = nil,
             subitems: [OutlineItem] = []) {
            self.title = title
            self.subitems = subitems
            self.outlineViewController = viewController
        }
        func hash(into hasher: inout Hasher) {
            hasher.combine(identifier)
        }
        static func == (lhs: OutlineItem, rhs: OutlineItem) -> Bool {
            return lhs.identifier == rhs.identifier
        }
        private let identifier = UUID()
    }
    // ※1
    var dataSource: UICollectionViewDiffableDataSource<Section, OutlineItem>! = nil
    var outlineCollectionView: UICollectionView! = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "Modern Collection Views"
        configureCollectionView()
        configureDataSource()
    }
    
    private lazy var menuItems: [OutlineItem] = {
        return [
            OutlineItem(title: "Compositional Layout", subitems: [
                OutlineItem(title: "Getting Started", subitems: [
                    OutlineItem(title: "Grid", viewController: GridViewController.self),
                    OutlineItem(title: "Inset Items Grid",
                                viewController: InsetItemsGridViewController.self),
                    OutlineItem(title: "Two-Column Grid", viewController: TwoColumnViewController.self),
                    OutlineItem(title: "Per-Section Layout", subitems: [
                        OutlineItem(title: "Distinct Sections",
                                    viewController: DistinctSectionsViewController.self),
                        OutlineItem(title: "Adaptive Sections",
                                    viewController: AdaptiveSectionsViewController.self)
                        ])
                    ]),
                OutlineItem(title: "Advanced Layouts", subitems: [
                    OutlineItem(title: "Supplementary Views", subitems: [
                        OutlineItem(title: "Item Badges",
                                    viewController: ItemBadgeSupplementaryViewController.self),
                        OutlineItem(title: "Section Headers/Footers",
                                    viewController: SectionHeadersFootersViewController.self),
                        OutlineItem(title: "Pinned Section Headers",
                                    viewController: PinnedSectionHeaderFooterViewController.self)
                        ]),
                    OutlineItem(title: "Section Background Decoration",
                                viewController: SectionDecorationViewController.self),
                    OutlineItem(title: "Nested Groups",
                                viewController: NestedGroupsViewController.self),
                    OutlineItem(title: "Orthogonal Sections", subitems: [
                        OutlineItem(title: "Orthogonal Sections",
                                    viewController: OrthogonalScrollingViewController.self),
                        OutlineItem(title: "Orthogonal Section Behaviors",
                                    viewController: OrthogonalScrollBehaviorViewController.self)
                        ])
                    ]),
                OutlineItem(title: "Conference App", subitems: [
                    OutlineItem(title: "Videos",
                                viewController: ConferenceVideoSessionsViewController.self),
                    OutlineItem(title: "News", viewController: ConferenceNewsFeedViewController.self)
                    ])
            ]),
            OutlineItem(title: "Diffable Data Source", subitems: [
                OutlineItem(title: "Mountains Search", viewController: MountainsViewController.self),
                OutlineItem(title: "Settings: Wi-Fi", viewController: WiFiSettingsViewController.self),
                OutlineItem(title: "Insertion Sort Visualization",
                            viewController: InsertionSortViewController.self),
                OutlineItem(title: "UITableView: Editing",
                            viewController: TableViewEditingViewController.self)
                ]),
            OutlineItem(title: "Lists", subitems: [
                OutlineItem(title: "Simple List", viewController: SimpleListViewController.self),
                OutlineItem(title: "Reorderable List", viewController: ReorderableListViewController.self),
                OutlineItem(title: "List Appearances", viewController: ListAppearancesViewController.self),
                OutlineItem(title: "List with Custom Cells", viewController: CustomCellListViewController.self)
            ]),
            OutlineItem(title: "Outlines", subitems: [
                OutlineItem(title: "Emoji Explorer", viewController: EmojiExplorerViewController.self),
                OutlineItem(title: "Emoji Explorer - List", viewController: EmojiExplorerListViewController.self)
            ]),
            OutlineItem(title: "Cell Configurations", subitems: [
                OutlineItem(title: "Custom Configurations", viewController: CustomConfigurationViewController.self)
            ])
        ]
    }()
    
}
```
##### ※１　`dataSource`を定義している部分について
`dataSource`は`UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> : NSObject where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable`となるため、パラメータの型が合うようにSectionとItemの型を定義する必要がある
ドキュメント：[UICollectionViewDiffableDataSource](https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource)


#### 次に`extension`内を見ていきます

```swift:OutlineViewController
extension OutlineViewController {

    func configureCollectionView() {
        // ※2
        let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: generateLayout())
        view.addSubview(collectionView)
        collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
        collectionView.backgroundColor = .systemGroupedBackground
        self.outlineCollectionView = collectionView
        collectionView.delegate = self
    }

    func configureDataSource() {
        // ※3
        let containerCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, OutlineItem> { (cell, indexPath, menuItem) in
            // Populate the cell with our item description.
            var contentConfiguration = cell.defaultContentConfiguration()
            contentConfiguration.text = menuItem.title
            contentConfiguration.textProperties.font = .preferredFont(forTextStyle: .headline)
            cell.contentConfiguration = contentConfiguration
            
            let disclosureOptions = UICellAccessory.OutlineDisclosureOptions(style: .header)
            cell.accessories = [.outlineDisclosure(options: disclosureOptions)]
            cell.backgroundConfiguration = UIBackgroundConfiguration.clear()
        }
        // ※3
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, OutlineItem> { cell, indexPath, menuItem in
            // Populate the cell with our item description.
            var contentConfiguration = cell.defaultContentConfiguration()
            contentConfiguration.text = menuItem.title
            cell.contentConfiguration = contentConfiguration
            cell.backgroundConfiguration = UIBackgroundConfiguration.clear()
        }
        // ※3
        dataSource = UICollectionViewDiffableDataSource<Section, OutlineItem>(collectionView: outlineCollectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, item: OutlineItem) -> UICollectionViewCell? in
            // Return the cell.
            if item.subitems.isEmpty {
                return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
            } else {
                return collectionView.dequeueConfiguredReusableCell(using: containerCellRegistration, for: indexPath, item: item)
            }
        }

        // load our initial data
        // ※4
        let snapshot = initialSnapshot()
        self.dataSource.apply(snapshot, to: .main, animatingDifferences: false)
    }
    // ※2
    func generateLayout() -> UICollectionViewLayout {
        let listConfiguration = UICollectionLayoutListConfiguration(appearance: .sidebar)
        let layout = UICollectionViewCompositionalLayout.list(using: listConfiguration)
        return layout
    }
    // ※4
    func initialSnapshot() -> NSDiffableDataSourceSectionSnapshot<OutlineItem> {
        var snapshot = NSDiffableDataSourceSectionSnapshot<OutlineItem>()

        func addItems(_ menuItems: [OutlineItem], to parent: OutlineItem?) {
            snapshot.append(menuItems, to: parent)
            for menuItem in menuItems where !menuItem.subitems.isEmpty {
                addItems(menuItem.subitems, to: menuItem)
            }
        }
        
        addItems(menuItems, to: nil)
        return snapshot
    }

}
```

##### ※2 コレクションビューのレイアウトを定義している部分について
コレクションビューのイニシャライズ時に使っている`generateLayout()`内の
`UICollectionLayoutListConfiguration(appearance: .sidebar)`
`UICollectionViewCompositionalLayout.list(using: listConfiguration)`
はリストセクションだけを含むコンポジションレイアウトを作成する際に使用する方法です
ドキュメント：[UICollectionLayoutListConfiguration](https://developer.apple.com/documentation/uikit/uicollectionlayoutlistconfiguration)

`UICollectionLayoutListConfiguration(appearance: .sidebar)`は他にも👇のように設定可能
###### `(appearance: .plain)`
<img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/48512584-d5aa-1cb1-f6e6-73597c73d6f1.png"> <img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/79654f46-4836-1a8f-f3e2-68ea536f4234.png">

###### `(appearance: .grouped)`
<img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/e1b15745-4b2a-3f54-72cb-8b6a02deffad.png"> <img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/8f58a7b0-1612-109a-47f5-38dbcbdeb7d4.png">

###### `(appearance: .insetGrouped)`
<img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/1c053812-4e06-22d1-f966-0c8034ee8c19.png"> <img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/7d9bacaa-97e9-d8bc-1b90-626012b7bb09.png">

###### `(appearance: .sidebar)`(比較のため)
<img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/b57e9c82-15fe-6d17-d972-f429c5a8db1b.png"> <img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/9be7106b-48c6-8477-fc56-7bb2c057d1c0.png">

###### `(appearance: .sidebarPlain)`
<img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/339783b9-469d-746b-09b1-554aca7f00f7.png"> <img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/f465ad21-c1a9-3538-5dd3-95b95684188a.png">


##### ※3　セルを生成する部分について
セルの生成は
```swift:ドキュメント
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { cell, indexPath, item in
    
    var contentConfiguration = cell.defaultContentConfiguration()
    
    contentConfiguration.text = "\(item)"
    contentConfiguration.textProperties.color = .lightGray
    
    cell.contentConfiguration = contentConfiguration
}
```
を使います
今回のサンプルでは、親セルと子セルの二種類（`containerCellRegistration`と`cellRegistration`）を生成しています
ドキュメント：[UICollectionView.CellRegistration](https://developer.apple.com/documentation/uikit/uicollectionview/cellregistration)

そして実際に描画する部分は
```swift:ドキュメント
dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) {
    (collectionView: UICollectionView, indexPath: IndexPath, itemIdentifier: Int) -> UICollectionViewCell? in
    
    return collectionView.dequeueConfiguredReusableCell(using: cellRegistration,
                                                        for: indexPath,
                                                        item: itemIdentifier)
}
```
これでCellを返します
ドキュメント：[UICollectionViewDiffableDataSource](https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource)

##### ※4 セルに初期値を与える部分について
コレクションビューに初期値を与える方法は👇の通り`NSDiffableDataSourceSectionSnapshot`を生成して`dataSource.apply`する

```swift:ドキュメント
for section in Section.allCases {
    // Create a section snapshot
    var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<String>()
    
    // Populate the section snapshot
    sectionSnapshot.append(["Food", "Drinks"])
    sectionSnapshot.append(["🍏", "🍓", "🥐"], to: "Food")
    
    // Apply the section snapshot
    dataSource.apply(sectionSnapshot,
                     to: section,
                     animatingDifferences: true)
}
```
ドキュメント：[NSDiffableDataSourceSectionSnapshot](https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesectionsnapshot)

#### 最後に`UICollectionViewDelegate`の`extension`を見ていきます

```swift:OutlineViewController
extension OutlineViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        guard let menuItem = self.dataSource.itemIdentifier(for: indexPath) else { return }
        
        collectionView.deselectItem(at: indexPath, animated: true)
        
        if let viewController = menuItem.outlineViewController {
            navigationController?.pushViewController(viewController.init(), animated: true)
        }     
    }
}
```

セルタップ時の処理は`collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)`と、お馴染みのものを使っている
ただし、メソッド内では`UICollectionViewDiffableDataSource`の`func itemIdentifier(for indexPath: IndexPath) -> ItemIdentifierType?`で`menuItem`を取得している

## 感想
まだまだサンプルコードを見ていかないと理解したとは言えませんが、初期表示画面の作りだけでも新しい要素がてんこ盛りで混乱しました。
引き続き`Modern Collection Views`と向き合っていきたいと思います。

## ちなみに
iOS15からはコレクションビューの描画パフォーマンスが向上しているとか

https://developer.apple.com/videos/play/wwdc2021/10252/


# 参考

https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views

https://qiita.com/netetahito/items/3aafc9e02918b4a92895

https://dev.classmethod.jp/articles/wwdc-20-lists-in-uicollectionview/