UIContentViewを使ってカスタムコレクションセルを実装

`UIContentView`を使ってカスタムセルを実装する方法を紹介します。
[こちら](https://developer.apple.com/tutorials/app-dev-training/editing-reminders)のリンクを参考にしました。

## 環境
`Xcode 14.2`

## 実装
以下、実装を進めます。
画像のような完成形を想定してます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551042/af66d328-59af-a42d-dbaf-aaab31532026.png" width="300" />


タスクのタイトルを編集する画面です。


### レイアウト設定
コレクションビューのレイアウトを設定します。
`insetGrouped`を指定しました。

```swift:ReminderViewController.swift
import UIKit

class ReminderViewController: UICollectionViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let listLayout = listLayout()
        collectionView.collectionViewLayout = listLayout
    }

    private func listLayout() -> UICollectionViewLayout {
        var listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        listConfiguration.showsSeparators = false
        listConfiguration.headerMode = .firstItemInSection
        return UICollectionViewCompositionalLayout.list(using: listConfiguration)
    }
}
```

### セル登録
セルを登録する関数を準備します。

```swift:ReminderViewController.swift
import UIKit

class ReminderViewController: UICollectionViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let listLayout = listLayout()
        collectionView.collectionViewLayout = listLayout

        let cellRegistration = UICollectionView.CellRegistration(handler: cellRegistrationHandler)
    }

    private func listLayout() -> UICollectionViewLayout {
        var listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        listConfiguration.showsSeparators = false
        listConfiguration.headerMode = .firstItemInSection
        return UICollectionViewCompositionalLayout.list(using: listConfiguration)
    }

    private func cellRegistrationHandler(cell: UICollectionViewListCell, indexPath: IndexPath, item: String) {
    }
}
```

### Section
今回はタイトルセクションの一つのみです。

```swift:ReminderViewController+Action.swift
import Foundation

import Foundation

extension ReminderViewController {
    enum Section: Int, Hashable {
        case title

        var name: String {
            switch self {
            case .title:
                return NSLocalizedString("Title", comment: "")
            }
        }
    }
}

```

### Row
コレクションビューの行はヘッダーとテキストフィールドの2種類です。

```swift:ReminderViewController+Row.swift
import UIKit

extension ReminderViewController {
    enum Row: Hashable {
        case header(String)
        case editText(String?)

        var textStyle: UIFont.TextStyle {
            switch self {
            default: return .subheadline
            }
        }
    }
}
```

### DataSource
`viewDidLoad`でDataSourceの設定を行います。

```swift:ReminderViewController.swift
import UIKit

class ReminderViewController: UICollectionViewController {
    private typealias DataSource = UICollectionViewDiffableDataSource<Section, Row>

    private var dataSource: DataSource!

    override func viewDidLoad() {
        super.viewDidLoad()

        let listLayout = listLayout()
        collectionView.collectionViewLayout = listLayout

        let cellRegistration = UICollectionView.CellRegistration(handler: cellRegistrationHandler)
        dataSource = DataSource(collectionView: collectionView, cellProvider: { (collectionView: UICollectionView, indexPath: IndexPath, itemIdentifier: Row) in
            return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
        })
    }

    private func listLayout() -> UICollectionViewLayout {
        var listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        listConfiguration.showsSeparators = false
        listConfiguration.headerMode = .firstItemInSection
        return UICollectionViewCompositionalLayout.list(using: listConfiguration)
    }

    private func cellRegistrationHandler(cell: UICollectionViewListCell, indexPath: IndexPath, row: Row) {
    }
}
```

### セル設定
セルの種類ごとに実行する設定処理を切り替えます。

```swift:ReminderViewController.swift
import UIKit

class ReminderViewController: UICollectionViewController {
    private typealias DataSource = UICollectionViewDiffableDataSource<Section, Row>

    private var dataSource: DataSource!

    override func viewDidLoad() {
        super.viewDidLoad()

        let listLayout = listLayout()
        collectionView.collectionViewLayout = listLayout

        let cellRegistration = UICollectionView.CellRegistration(handler: cellRegistrationHandler)
        dataSource = DataSource(collectionView: collectionView, cellProvider: { (collectionView: UICollectionView, indexPath: IndexPath, itemIdentifier: Row) in
            return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
        })
    }

    private func listLayout() -> UICollectionViewLayout {
        var listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        listConfiguration.showsSeparators = false
        listConfiguration.headerMode = .firstItemInSection
        return UICollectionViewCompositionalLayout.list(using: listConfiguration)
    }

    private func cellRegistrationHandler(cell: UICollectionViewListCell, indexPath: IndexPath, row: Row) {
        let section = section(for: indexPath)

        switch (section, row) {
        case (_, .header(let title)):
            break // TODO: Headerセルの設定を行う

        case (.title, .editText(let title)):
            break // TODO: タイトル編集のセル設定を行う

        default:
            fatalError()
        }
    }

    private func section(for indexPath: IndexPath) -> Section {
        let sectionNumber = indexPath.section
        guard let section = Section(rawValue: sectionNumber) else { fatalError() }
        return section
    }
}
```

### Headerセル設定
```swift:ReminderViewController+CellConfiguration.swift
extension ReminderViewController {
    func headerConfiguration(for cell: UICollectionViewListCell, with title: String) -> UIListContentConfiguration {
        var contentConfiguration = cell.defaultContentConfiguration()
        contentConfiguration.text = title
        return contentConfiguration
    }
}
```

### TextFieldContentView
`UITextField`をセルで扱えるようにします。

```swift:TextFieldContentView.swift
import UIKit

class TextFieldContentView: UIView, UIContentView {
    struct Configuration: UIContentConfiguration {
        var text: String? = nil

        func makeContentView() -> UIView & UIContentView {
            TextFieldContentView(self)
        }

        func updated(for state: UIConfigurationState) -> TextFieldContentView.Configuration {
            self
        }
    }

    private let textField = UITextField()
    var configuration: UIContentConfiguration {
        didSet {
            configure(configuration: configuration)
        }
    }

    init(_ configuration: UIContentConfiguration) {
        self.configuration = configuration

        super.init(frame: .zero)

        addPinnedSubview(textField, insets: UIEdgeInsets(top: .zero, left: 16, bottom: .zero, right: 16))
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override var intrinsicContentSize: CGSize {
        CGSize(width: .zero, height: 44)
    }

    func configure(configuration: UIContentConfiguration) {
        guard let configuration = configuration as? Configuration else { return }
        textField.text = configuration.text
    }
}

extension UICollectionViewListCell {
    func textFieldConfiguration() -> TextFieldContentView.Configuration {
        TextFieldContentView.Configuration()
    }
}
```

### タイトル設定

```swift:ReminderViewController+CellConfiguration.swift
import UIKit

extension ReminderViewController {
    func headerConfiguration(for cell: UICollectionViewListCell, with title: String) -> UIListContentConfiguration {
        var contentConfiguration = cell.defaultContentConfiguration()
        contentConfiguration.text = title
        return contentConfiguration
    }

    func titleConfiguration(for cell: UICollectionViewListCell, with title: String?) -> TextFieldContentView.Configuration {
        var contentConfiguration = cell.textFieldConfiguration()
        contentConfiguration.text = title
        return contentConfiguration
    }
}
```

### Snapshot
最後にSnapshotを更新することで画面に表示させます。

```swift:ReminderViewController.swift
import UIKit

class ReminderViewController: UICollectionViewController {
    private typealias DataSource = UICollectionViewDiffableDataSource<Section, Row>
    private typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Row>
    
    private var dataSource: DataSource!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let listLayout = listLayout()
        collectionView.collectionViewLayout = listLayout
        
        let cellRegistration = UICollectionView.CellRegistration(handler: cellRegistrationHandler)
        dataSource = DataSource(collectionView: collectionView, cellProvider: { (collectionView: UICollectionView, indexPath: IndexPath, itemIdentifier: Row) in
            return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
        })
        
        updateSnapshotForEditing()
    }
    
    private func listLayout() -> UICollectionViewLayout {
        var listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        listConfiguration.showsSeparators = false
        listConfiguration.headerMode = .firstItemInSection // TODO: TARO 確認
        return UICollectionViewCompositionalLayout.list(using: listConfiguration)
    }
    
    private func cellRegistrationHandler(cell: UICollectionViewListCell, indexPath: IndexPath, row: Row) {
        let section = section(for: indexPath)
        
        switch (section, row) {
        case (_, .header(let title)):
            cell.contentConfiguration = headerConfiguration(for: cell, with: title)
            
        case (.title, .editText(let title)):
            cell.contentConfiguration = titleConfiguration(for: cell, with: title)
        }
    }
    
    private func updateSnapshotForEditing() {
        var snapshot = Snapshot()
        snapshot.appendSections([.title])
        snapshot.appendItems([.header(Section.title.name), .editText("")], toSection: .title)
        dataSource.apply(snapshot)
    }
    
    private func section(for indexPath: IndexPath) -> Section {
        let sectionNumber = indexPath.section
        guard let section = Section(rawValue: sectionNumber) else { fatalError() }
        return section
    }
}

```
以上です。
ソースコードは[こちら](https://github.com/h-taro/ui-content-configuration-demo)です。

## 参考
https://developer.apple.com/tutorials/app-dev-training/editing-reminders