はじめに
公式で公開されているImplementing Modern Collection Views
のサンプルコードを見てできることの多さに驚き、最新のコレクションビューのAPIについて調べてみることにしました。
コレクションビューの進化
👇こちらのWWDC20のプレゼンで詳しく説明されていました
iOS12まで(私の理解していたコレクションビュー)
iOS13からData
とLayout
が変わってこうなった
Compositional Layout
について👇
iOS14からはさらに進化した
Section Snapshots
について👇
List Configuration
について👇
List Cell
とView Configuration
について👇
サンプルコードを読んでみる
まずはサンプルアプリを表示します
Modern Collection Views
- Compositional Layout
- Diffable Data Source
- Lists
- Outlines
- Cell Configuratons
の構成となっていますが、初期表示画面を見ていくだけでも理解が深まると思いましたので、初期表示画面のサンプルコードを見ていきます
初期表示画面のViewController
OutlineViewController
となっていますが、これは初期画面(テーブルビューの見た目のコレクションビュー)のもので、項目のOutlines
とは別です💡
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)
])
]
}()
}
※1 dataSource
を定義している部分について
dataSource
はUICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> : NSObject where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable
となるため、パラメータの型が合うようにSectionとItemの型を定義する必要がある
ドキュメント:UICollectionViewDiffableDataSource
次にextension
内を見ていきます
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
UICollectionLayoutListConfiguration(appearance: .sidebar)
は他にも👇のように設定可能
(appearance: .plain)
(appearance: .grouped)
(appearance: .insetGrouped)
(appearance: .sidebar)
(比較のため)
(appearance: .sidebarPlain)
※3 セルを生成する部分について
セルの生成は
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
そして実際に描画する部分は
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
※4 セルに初期値を与える部分について
コレクションビューに初期値を与える方法は👇の通りNSDiffableDataSourceSectionSnapshot
を生成してdataSource.apply
する
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
最後にUICollectionViewDelegate
のextension
を見ていきます
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からはコレクションビューの描画パフォーマンスが向上しているとか
参考