完成イメージ
パターン1(全てのセクションで同じ名前、同じデータ形式を表示する場合)
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]
}
}
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からの差分のみ記載)
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
}
}
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からの差分のみ記載)
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
}
}
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からの差分のみ記載)
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からの差分のみ記載)
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の違い
RxDataSources/TodolistDetailViewModel.swift
public protocol SectionModelType {
associatedtype Item
var items: [Item] { get }
init(original: Self, items: [Item])
}
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
}
}