🔬

UICollectionLayoutListConfigurationのheaderMode=.firstItemInSection観測隊

2023/04/30に公開

はじめに

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 を使う。UICollectionLayoutListConfiguration はiOS14からなのでわりと新しいものである。
ここで listConfiguration.headerMode = .firstItemInSection というのが、ひとつめのものをヘッダーとして使う設定にしている箇所である。

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

UICollectionLayoutListConfiguration(appearance: .plain)

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

.plain

.grouped

.insetGrouped

.sidebar

.sidebarPlain

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

タップする

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

我々の任務はヘッダーの扱いの調査だったので今日の観測はここまでである。ここからはソースコードの他の部分について書いたもの。記事を書いている時点でモダンな UICollectionView の書き方に詳しいわけではないので、古い書き方が混ざっている。

UICollectionViewその他

collectionView.register(
	UICollectionViewListCell.self, 
	forCellWithReuseIdentifier: cellId)
collectionView.dataSource = self
collectionView.delegate = self

セルの形式を UICollectionViewListCell にする。cellIdCell という文字列。

データソース

func numberOfSections(
	in collectionView: UICollectionView) -> Int {
    prefectures.count
}
func collectionView(
	_ collectionView: UICollectionView, 
	numberOfItemsInSection section: Int) -> Int {
   prefectures[section].count
}
func collectionView(
	_ collectionView: UICollectionView, 
	cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(
	    withReuseIdentifier: cellId, 
	    for: indexPath) as! UICollectionViewListCell
    
    var cellConfiguration = cell.defaultContentConfiguration()
    cellConfiguration.text = prefectures[indexPath.section][indexPath.row]
    cell.contentConfiguration = cellConfiguration
    
    return cell
}

セルを取得するが先ほど UICollectionViewListCell で取得できるようにしたので、それが返ってくる。その UICollectionViewListCelldefaultContentConfiguration()UIListContentConfiguration を返す。これはセルの中身を設定するもので、文字列や画像などの入れ物があらかじめ用意されている。

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

  • UICollectionLayoutListConfiguration UICollectionViewの性質を扱うコンフィグレーション
  • UICollectionViewListCell セルの型
  • UIListContentConfiguration セルの性質を扱うコンフィグレーション

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

ViewController全体


import UIKit

class ViewController: UIViewController {

    let prefectures = [
        ["北海道", "北海道"],
        ["東北", "青森", "岩手", "秋田", "宮城", "山形", "福島"],
        ["関東", "茨城", "栃木", "群馬", "埼玉", "千葉", "東京", "神奈川"],
        ["甲信越", "新潟", "長野", "山梨"],
        ["北陸", "富山", "石川", "福井"],
        ["東海", "岐阜", "静岡", "愛知", "三重"],
        ["近畿", "滋賀", "京都", "奈良", "大阪", "和歌山", "兵庫"],
        ["中国", "鳥取", "島根", "岡山", "広島", "山口"],
        ["四国", "香川", "徳島", "愛媛", "高知"],
        ["九州", "福岡", "佐賀", "長崎", "大分", "熊本", "宮崎", "鹿児島"],
        ["沖縄", "沖縄"]
    ]
    
    let cellId = "Cell"
    var collectionView: UICollectionView = {
        var listConfiguration = UICollectionLayoutListConfiguration(appearance: .sidebar)
        listConfiguration.headerMode = .firstItemInSection 
        let simpleLayout = UICollectionViewCompositionalLayout.list(using: listConfiguration)
        return UICollectionView(frame: .zero, collectionViewLayout: simpleLayout)
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.register(UICollectionViewListCell.self, forCellWithReuseIdentifier: cellId)
        
        collectionView.dataSource = self
        collectionView.delegate = self
        
        view.addSubview(collectionView)
        
        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: UICollectionViewDataSource {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        prefectures.count
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
       prefectures[section].count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! UICollectionViewListCell
        
        var cellConfiguration = cell.defaultContentConfiguration()
        cellConfiguration.text = prefectures[indexPath.section][indexPath.row]
        cell.contentConfiguration = cellConfiguration

        return cell
    }
}

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

Discussion