＜Swift＞RxDataSourcesでヘッダー・フッター付きのUICollectionViewを実装

# 完成イメージ
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136142/8acccf36-1ef1-a354-890f-815049597740.png" width="300">

# パターン1（全てのセクションで同じ名前、同じデータ形式を表示する場合）

```swift:TodolistDetailViewModel.swift
import Foundation
import RxSwift
import RxCocoa
import RxDataSources

struct SectionOfTodolist {
    // headerはSectionModelTypeに設定されない。
    // 任意の文字列を設定しておき、ヘッダーセルを作るときに参照するために定義。
    // 参照の仕方：dataSource.sectionModels[indexPath.section].header
    var header: String
    var items: [Item]
}

extension SectionOfTodolist: SectionModelType {
    typealias Item = Todo
    init(original: SectionOfTodolist, items: [SectionOfTodolist.Item]) {
        self = original
        self.items = items
    }
}

class TodolistDetailViewModel {
    let items = BehaviorRelay<[SectionOfTodolist]>(value: [])

    func updateItems() {
        let todolist = getTodolist()
        let sections = convertToSectionModels(srcArray: todolist.todoArray)
        items.accept(sections)
    }
    
    private func getTodolist() -> Todolist {
        // ダミーデータ
        var todoArray: [Todo] = []
        todoArray.append(Todo(title: "テストTODO1", expireDate: Date()))
        todoArray.append(Todo(title: "テストTODO2", expireDate: Date()))
        todoArray.append(Todo(title: "テストTODO3", expireDate: Date()))
        let ret = Todolist(name: "ダミーTODOリスト", color: .blue, todoArray: todoArray)
        return ret
    }
    
    private func convertToSectionModels(srcArray: [Todo]) -> [SectionOfTodolist] {
        let section =
            SectionOfTodolist(header: "test header"
                , items: srcArray)
        let section2 =
                    SectionOfTodolist(header: "test header2"
                        , items: srcArray)
        return [section, section2]
    }
}
```

```swift:TodolistDetailViewController.swift
import UIKit
import RxSwift
import RxDataSources

class TodolistDetailViewController: UIViewController {

    @IBOutlet weak var collectionView: UICollectionView!
    
    private lazy var dataSource = RxCollectionViewSectionedReloadDataSource<SectionOfTodolist>(
    configureCell: configureCell
    ,configureSupplementaryView: titleForHeaderInSection)

    // カスタムセルを設定
    private lazy var configureCell: RxCollectionViewSectionedReloadDataSource<SectionOfTodolist>.ConfigureCell = { [weak self] (_, collectionView, indexPath, todo) in
        guard let strongSelf = self else { return UICollectionViewCell() }
            return strongSelf.todolistCell(indexPath: indexPath, todo: todo)
    }
    
    // セクションヘッダータイトルを設定
    private lazy var titleForHeaderInSection: RxCollectionViewSectionedReloadDataSource<SectionOfTodolist>.ConfigureSupplementaryView = { [weak self] (dataSource, collectionView, kind, indexPath) in
        guard let strongSelf = self else { return UICollectionReusableView() }
        return strongSelf.headerCell(indexPath: indexPath, kind: kind)
    }

    private var viewModel: TodolistDetailViewModel!
    
    private let todolistId: Int
    
    private let disposeBag = DisposeBag()
    
    init(todolistId: Int) {
        self.todolistId = todolistId
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupViewController()
        setupCollectionView()
        setupViewModel()
    }
}

extension TodolistDetailViewController {
    private func setupViewController() {
        self.navigationItem.title = "ダミーTODOリスト"
        self.view.backgroundColor = .systemBlue
    }

    private func setupCollectionView() {
        collectionView.contentInset.top = TodolistDetailCollectionViewCell.cellMargin
        collectionView.register(UINib(nibName: "TodolistDetailCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "TodolistDetailCollectionViewCell")
        collectionView.register(
            UICollectionReusableView.self
            , forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader
            , withReuseIdentifier: "Section")
        collectionView.rx.setDelegate(self).disposed(by: disposeBag)
        
        // ヘッダサイズを指定
        let flowLayout = UICollectionViewFlowLayout()
        flowLayout.headerReferenceSize = CGSize(width: self.collectionView.frame.width, height: 40)
        collectionView.collectionViewLayout = flowLayout
        
        // タップ時のイベント
        collectionView.rx.itemSelected
            .map { [weak self] indexPath -> Todo? in
                return self?.dataSource[indexPath]
            }
            .subscribe(onNext: { [weak self] item in
                guard let item = item else { return }
                    self?.presentTodoDetaillViewController()
            })
            .disposed(by: disposeBag)
    }

    private func setupViewModel() {
        viewModel = TodolistDetailViewModel()
        viewModel.items
            .asDriver()
            .drive(collectionView.rx.items(dataSource: dataSource))
            .disposed(by: disposeBag)
        viewModel.updateItems()
    }

    private func todolistCell(indexPath: IndexPath, todo: Todo) -> UICollectionViewCell {
        if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TodolistDetailCollectionViewCell", for: indexPath ) as? TodolistDetailCollectionViewCell{
            cell.update(todoTitle: todo.title, todoExpireDate: todo.expireDate)
                        
            // チェックボックスタップ時
            let checkButtonObservable = cell.checkButton.rx.tap.asObservable()
            checkButtonObservable.subscribe(
                onNext: { [weak self] in
                    cell.isCheck = !cell.isCheck
                    let image = cell.isCheck ? UIImage(systemName: "checkmark.square") : UIImage(systemName: "square")
                    cell.checkButton.setImage(image, for: .normal)
                }
            ).disposed(by: cell.disposeBag)
            
            return cell
        }
        return UICollectionViewCell()
    }

    private func headerCell(indexPath: IndexPath, kind: String) -> UICollectionReusableView {
        let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Section", for: indexPath)
        headerView.backgroundColor = .yellow
        let label = UILabel(frame: CGRect(x:0, y:0, width:self.collectionView.frame.width, height:20))
        label.textColor = UIColor.black
        label.textAlignment = .center
        label.text = dataSource.sectionModels[indexPath.section].header
        headerView.addSubview(label)
        return headerView
    }

    private func presentTodoDetaillViewController() {
        let todoId = 0
        let vc = TodoViewController(todoId: todoId)
        self.navigationController?.pushViewController(vc, animated: true)
    }
}

extension TodolistDetailViewController: UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        let item = dataSource[section]
            return UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let item = dataSource[indexPath]
            let width = collectionView.bounds.width - (TodolistDetailCollectionViewCell.cellMargin * 2)
            return CGSize(width: width, height: TodolistDetailCollectionViewCell.cellHeight)
    }
}
```
■参考にしたサイト
http://m.hangge.com/news/cache/detail_2005.html
https://www.jianshu.com/p/8b9cb8d57edd

# パターン2（セルに表示するデータ形式をセクションによって変える場合）
※例：(パターン1からの差分のみ記載)

```swift:TodolistDetailViewModel.swift
extension SectionOfTodolist: SectionModelType {
    // ＜変更＞
    typealias Item = TodolistItem
    ...省略
}

// ＜追加＞
enum TodolistItem {
    case todo(todo: Todo)
    case memo(memo: Memo)
}

class TodolistDetailViewModel {
    ...省略
    func updateItems() {
        let todolist = getTodolist()
        // ＜変更＞
        let sections1 = convertTodoToSectionModels(srcArray: todolist.todoArray)
        let sections2 = convertMemoToSectionModels(srcArray: memolist.memoArray)
        items.accept([sections1, sections2])
    }
    
    // ＜追加＞
    private func convertTodoToSectionModel(srcArray: [Todo]) -> SectionOfTodolist {
        let items = srcArray.map {
            TodolistItem.todo(todo: $0)
        }
        let section =
            SectionOfTodolist(header: "TODOカテゴリー"
                , items: items)
        
        return section
    }
    
    // ＜追加＞
    private func convertMemoToSectionModel(srcArray: [Memo]) -> SectionOfTodolist {
        let items = srcArray.map {
            TodolistItem.memo(memo: $0)
        }
        let section =
            SectionOfTodolist(header: "MEMOカテゴリー"
                , items: items)
        return section
    }
}
```

```swift:TodolistDetailViewController.swift
class TodolistDetailViewController: UIViewController {
...省略...
    // カスタムセルを設定
    private lazy var configureCell: RxCollectionViewSectionedReloadDataSource<SectionOfTodolist>.ConfigureCell = { [weak self] (_, collectionView, indexPath, item) in
        guard let strongSelf = self else { return UICollectionViewCell() }
        switch item {
        // ＜変更＞
        case .todo(let todo):
            return strongSelf.todolistCell(indexPath: indexPath, todo: todo)
        case .memo(let memo):
            return strongSelf.todolistCell(indexPath: indexPath, todo: memo)
        }
    }
...省略
    private func setupCollectionView() {
...省略
        // タップ時のイベント
        collectionView.rx.itemSelected
            // ＜変更＞
            .map { [weak self] indexPath -> TodolistItem? in
                return self?.dataSource[indexPath]
            }
            .subscribe(onNext: { [weak self] item in
                guard let item = item else { return }
                switch item {
                // ＜変更＞
                case .todo(let todo):
                    self?.presentTodoDetaillViewController()
                case .memo(let memo):
                    self?.presentTodoDetaillViewController()
                }
            })
            .disposed(by: disposeBag)
    }
...省略
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let item = dataSource[indexPath]
        switch item {
        // ＜変更＞
        case .todo:
            let width = collectionView.bounds.width - (TodolistDetailCollectionViewCell.cellMargin * 2)
            return CGSize(width: width, height: TodolistDetailCollectionViewCell.cellHeight)
        case .memo:
            let width = collectionView.bounds.width - (TodolistDetailCollectionViewCell.cellMargin * 2)
            return CGSize(width: width, height: TodolistDetailCollectionViewCell.cellHeight)
        }
    }
}

```

# パターン3(セルに表示するデータ形式をセクションによって変える + セクションによってヘッダのタイトルを変える場合）

※例(パターン2からの差分のみ記載)

```swift:TodolistDetailViewModel.swift

struct SectionOfTodolist {
    // 変更
    var model: TodolistSection
    var items: [Item]
}

// 追加
enum TodolistSection {
    case category(name: String)
}

class TodolistDetailViewModel {
    ...省略
    private func convertTodoToSectionModel(srcArray: [Todo]) -> SectionOfTodolist {
        let items = srcArray.map {
            TodolistItem.todo(todo: $0)
        }
        // ＜変更＞
        let section = SectionOfTodolist(model: .category(name: "TODOカテゴリー"), items: items)
        return section
    }

    private func convertMemoToSectionModel(srcArray: [Memo]) -> SectionOfTodolist {
        let items = srcArray.map {
            TodolistItem.memo(memo: $0)
        }
        // ＜変更＞
        let section = SectionOfTodolist(model: .category(name: "MEMOカテゴリー"), items: items)
        return section
    }
}
```
```swift:TodolistDetailViewController.swift

class TodolistDetailViewController: UIViewController {
...省略...
    // セクションヘッダータイトルを設定
    private lazy var titleForHeaderInSection: RxCollectionViewSectionedReloadDataSource<SectionOfTodolist>.ConfigureSupplementaryView = { [weak self] (dataSource, collectionView, kind, indexPath) in
        
        guard let strongSelf = self else { return UICollectionReusableView() }
        // ＜変更＞
        let sectionOfTodolist = dataSource[indexPath.section]
        switch sectionOfTodolist.model {
        case .category:
            return strongSelf.headerCell(indexPath: indexPath, kind: kind)
        }
    }

    private func headerCell(indexPath: IndexPath, kind: String) -> UICollectionReusableView {
        
        let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath)
        headerView.backgroundColor = .yellow
        let label = UILabel(frame: CGRect(x:0, y:0, width:self.collectionView.frame.width, height:20))
        label.textColor = UIColor.black
        label.textAlignment = .center
        
        // ＜変更＞
        let sectionOfTodolist = dataSource[indexPath.section]
        switch sectionOfTodolist.model {
        case .category(let name):
            label.text = name
        }
        headerView.addSubview(label)
        return headerView
    }
}

extension TodolistDetailViewController: UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        // ＜変更＞
        let sectionOfTodolist = dataSource[section]
        switch sectionOfTodolist.model {
        case .category:
            return UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
        }
    }
}
```

# パターン4(セルに表示するデータ形式をセクションによって変える + セクションによってヘッダのタイトルを変える + フッターも表示する場合）
※例(パターン2からの差分のみ記載)

```swift:TodolistDetailViewController.swift
class TodolistDetailViewController: UIViewController {

    // ＜変更＞
    private lazy var dataSource = RxCollectionViewSectionedReloadDataSource<SectionOfTodolist>(
    configureCell: configureCell
    ,configureSupplementaryView: collectionReusableView)

    ...省略

    // ＜変更＞
    // セクションヘッダーとフッターセルを設定
    private lazy var collectionReusableView: RxCollectionViewSectionedReloadDataSource<SectionOfTodolist>.ConfigureSupplementaryView = { [weak self] (dataSource, collectionView, kind, indexPath) in
        ...省略
        case .category:
            // ＜変更＞
            return strongSelf.headerFooterCell(indexPath: indexPath, kind: kind)
        }
    }

    private func setupCollectionView() {
        ...省略
        // ＜変更＞
        collectionView.register(
        UICollectionReusableView.self
        , forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader
        , withReuseIdentifier: "Header")
        // ＜追加＞
        collectionView.register(
            UICollectionReusableView.self
            , forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter
            , withReuseIdentifier: "Footer")
        ...省略
        // ＜追加＞
        // フッターサイズを指定
        flowLayout.footerReferenceSize = CGSize(width: self.collectionView.frame.width, height: 40)
        ...省略
    }

    private func headerFooterCell(indexPath: IndexPath, kind: String) -> UICollectionReusableView {
        // ＜追加＞
        if (kind == "UICollectionElementKindSectionHeader") {
        ...省略
        // ＜追加＞
        } else {
            let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Footer", for: indexPath)
            footerView.backgroundColor = .yellow
            let sectionOfTodolist = dataSource[indexPath.section]
            switch sectionOfTodolist.model {
            case .category(let name):
                let button = UIButton(frame: CGRect(x:0, y:10, width:self.collectionView.frame.width, height:20))
                button.setTitle("TODOを追加", for: .normal)
                button.setTitleColor(.gray, for: .normal)
                footerView.addSubview(button)
                button.rx.tap.asObservable().subscribe(onNext: {
                    [weak self] in
                    self?.presentTodoDetaillViewController()
                }).disposed(by: disposeBag)
            }
            return footerView
        }
    }
    ...省略
}
```

# パターン5(パターン4をSectionModelでやる場合)
※例(パターン3からの差分のみ記載)

```swift:TodolistDetailViewModel.swift
// 削除
//struct SectionOfTodolist {
//    var header: String
//    var items: [Item]
//}
//
//extension SectionOfTodolist: SectionModelType {
//    typealias Item = TodolistItem
//
//    init(original: SectionOfTodolist, items: [SectionOfTodolist.Item]) {
//        self = original
//        self.items = items
//    }
//}

// 追加
typealias SectionOfTodolist = SectionModel<TodolistSection, TodolistItem>
```

# 補足：SectionModelTypeとSectionModelの違い

```swift:RxDataSources/TodolistDetailViewModel.swift
public protocol SectionModelType {
    associatedtype Item

    var items: [Item] { get }

    init(original: Self, items: [Item])
}
```

```swift:RxDataSources/SectionModel.swift
public struct SectionModel<Section, ItemType> {
    public var model: Section
    public var items: [Item]

    public init(model: Section, items: [Item]) {
        self.model = model
        self.items = items
    }
}

extension SectionModel
    : SectionModelType {
    public typealias Identity = Section
    public typealias Item = ItemType
    
    public var identity: Section {
        return model
    }
}

extension SectionModel
    : CustomStringConvertible {

    public var description: String {
        return "\(self.model) > \(items)"
    }
}

extension SectionModel {
    public init(original: SectionModel<Section, Item>, items: [Item]) {
        self.model = original.model
        self.items = items
    }
}
```