UIContentView
を使ってカスタムセルを実装する方法を紹介します。
こちらのリンクを参考にしました。
環境
Xcode 14.2
実装
以下、実装を進めます。
画像のような完成形を想定してます。

タスクのタイトルを編集する画面です。
レイアウト設定
コレクションビューのレイアウトを設定します。
insetGrouped
を指定しました。
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)
}
}
セル登録
セルを登録する関数を準備します。
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
今回はタイトルセクションの一つのみです。
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種類です。
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の設定を行います。
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) {
}
}
セル設定
セルの種類ごとに実行する設定処理を切り替えます。
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セル設定
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
をセルで扱えるようにします。
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()
}
}
タイトル設定
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を更新することで画面に表示させます。
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
}
}
以上です。
ソースコードはこちらです。
参考