🔬

UICollectionLayoutListConfigurationのheaderMode=.firstItemInSection観測隊

に公開

はじめに

UICollectionViewUITalbeView よりも出来ることが多いですよ、ということでAppleが推している UICollectionView
まずは次のものを見ていただきたい。あるソースコードとその実行結果(の一部)である。

「甲信越」など、ひとつめのデータがちょっと違う色と大きさで表示されている。これは、1個目のデータをヘッダー扱いにする機能を使っている。これは UICollectionView に備わっている機能である。今日はこれを観測していきたい。

UICollectionViewのコンフィグレーション

この仕様にするためには UICollectionView にコンフィグレーションというものを使う。コンフィグレーションとは仕様を細かく設定するものである。コンフィグレーションを使った UICollectionView のインスタンス生成の様子は次の通り。

var collectionView: UICollectionView = {
    var listConfiguration = 
	    UICollectionLayoutListConfiguration(
		    appearance: .plain)
    listConfiguration.headerMode = .firstItemInSection 
    let simpleLayout = 
	    UICollectionViewCompositionalLayout.list(
		    using: listConfiguration)
    return UICollectionView(
	    frame: .zero, 
	    collectionViewLayout: simpleLayout)
}()

コンフィグレーションの中でも UICollectionLayoutListConfiguration を使う。これはiOS14からなのでわりと新しいものである。
ここで listConfiguration.headerMode = .firstItemInSection というのが、ひとつめのものをヘッダーとして使う設定にしている箇所である。

UICollectionLayoutListConfigurationの設定よって見た目が違う

UICollectionLayoutListConfiguration(appearance: .plain)

の引数に与えるものによってヘッダーのデザインが変わる。

.plain

.grouped

.insetGrouped

.sidebar

.sidebarPlain

sidebarは文字が大きいですな。
新潟を選択状態にした理由は、選択していないと .plain.sidebarPlain はそもそも何が違うのかと思われてしまうから。

タップする

「甲信越」の部分をタップすると通常のセルと同じように collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
が走る。

我々の任務はヘッダーの扱いの調査だったので今日の観測はここまでである。ここからはソースコードの他の部分について書いたもの。

データソース

//リサイクル機能
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> {
    (cell, indexPath, identifier) in
    var configration = cell.defaultContentConfiguration()
    configration.text = identifier
    cell.contentConfiguration = configration
}
//データソースの作成
dataSource = UICollectionViewDiffableDataSource<Int, String>
    (collectionView: collectionView) {
    //セルの内容が決定
    (collectionView: UICollectionView, indexPath: IndexPath, identifier: String) -> UICollectionViewCell? in
    return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
}
//構成を決める
var snapshot = NSDiffableDataSourceSnapshot<Int, String>()
snapshot.appendSections((0..<prefectures.count).map{ $0 })
for i in 0..<prefectures.count {
    snapshot.appendItems(prefectures[i], toSection: i)
}
dataSource.apply(snapshot, animatingDifferences: false)

リサイクル機能では UICollectionViewListCell で取得できるようにしたので、それが用意されている。その UICollectionViewListCell インスタンスの defaultContentConfiguration()UIListContentConfiguration を返す。これはセルの中身を設定するもので、文字列や画像などの入れ物があらかじめ用意されている。

ちなみにこの新しいデータソースの書き方の場合 ["北海道", "北海道"] のように同じデータが2つあるとエラーになる、残念。

Listなんとか、ありすぎじゃね?

  • UICollectionLayoutListConfiguration UICollectionViewのレイアウトの性質を扱うコンフィグレーション
  • UICollectionViewCompositionalLayout.list レイアウト情報作成メソッド。リスト形式専用のもの。
  • UICollectionViewListCell セルの型
  • UIListContentConfiguration セルの性質を扱うコンフィグレーション

この4つの組み合わせ以外を試そうかとも思うがあまり面白い結果にならなさそう。

ViewController全体

import UIKit

class ViewController: UIViewController {
    
    let prefectures = [
        ["東北", "青森", "岩手", "秋田", "宮城", "山形", "福島"],
        ["関東", "茨城", "栃木", "群馬", "埼玉", "千葉", "東京", "神奈川"],
        ["甲信越", "新潟", "長野", "山梨"],
        ["北陸", "富山", "石川", "福井"],
        ["東海", "岐阜", "静岡", "愛知", "三重"],
        ["近畿", "滋賀", "京都", "奈良", "大阪", "和歌山", "兵庫"],
        ["中国", "鳥取", "島根", "岡山", "広島", "山口"],
        ["四国", "香川", "徳島", "愛媛", "高知"],
        ["九州", "福岡", "佐賀", "長崎", "大分", "熊本", "宮崎", "鹿児島"],
    ]
    
    var collectionView: UICollectionView = {
        var listConfiguration = UICollectionLayoutListConfiguration(appearance: .plain)
        listConfiguration.headerMode = .firstItemInSection
        let simpleLayout = UICollectionViewCompositionalLayout.list(using: listConfiguration)
        return UICollectionView(frame: .zero, collectionViewLayout: simpleLayout)
    }()
    var dataSource: UICollectionViewDiffableDataSource<Int, String>! = nil
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        collectionView.delegate = self
        
        view.addSubview(collectionView)
        configureDataSource()
        
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
        collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
        collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
        collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
    }
}

extension ViewController {
    
    func configureDataSource() {
        
        //リサイクル機能
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { (cell, indexPath, identifier) in
            
            var configration = cell.defaultContentConfiguration()
            configration.text = identifier
            cell.contentConfiguration = configration
        }
        
        //データソースの作成
        dataSource = UICollectionViewDiffableDataSource<Int, String> (collectionView: collectionView) {
            //セルの内容が決定
            (collectionView: UICollectionView, indexPath: IndexPath, identifier: String) -> UICollectionViewCell? in
            return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
        }
        
        //構成を決める
        var snapshot = NSDiffableDataSourceSnapshot<Int, String>()
        snapshot.appendSections((0..<prefectures.count).map{ $0 })
        for i in 0..<prefectures.count {
            snapshot.appendItems(prefectures[i], toSection: i)
        }
        dataSource.apply(snapshot, animatingDifferences: false)
    }
}

extension ViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        print("didSelect", prefectures[indexPath.section][indexPath.row])
    }
}

Discussion