xyk blog

最近は iOS 開発の記事が多めです。

UICollectionViewCell の Self-Sizing で動的にセルサイズを調整する

検証環境:
Xcode 12.4
Swift 5.3.2

UICollectionViewCell の Self-Sizing 機能でコンテンツに基づいて動的にセルサイズを調整する方法について。

まず今回 UICollectionView を使って実現したいレイアウトは横幅が画面幅(collectionView.frame.width)で、縦幅はセル内のコンテンツが可変であるためそれが収まるように動的に決まり、縦スクロールする UITableView のようなレイアウト。

セルサイズを自分で計算するのではなく、 Self-Sizing 機能を使って自動で調整されるようにしたい。

f:id:xyk:20210226130516p:plain

StoaryBoard 設定

  • StoryBoard で Root View に UICollectionView 及び UICollectionViewCell のビューを置く。
    UICollectionView と UICollectionViewDelegateFlowLayout は @IBOutlet でコード上の定義と接続する。

  • UICollectionViewCell の ContentView には Lines 0 の UILabel を置く。
    UILabel に ContentView の4辺と合わせる制約と、暫定の width の制約を付ける。
    そして UILabel と UILabel の width 制約は @IBOutlet でコード上の定義 maxWidthConstraint と接続する。

f:id:xyk:20210226131806p:plain

コードサンプル

セルフサイジングを有効にするにはUICollectionViewFlowLayout.estimatedItemSizeに 0 以外の値、通常はUICollectionViewFlowLayout.automaticSizeを設定する必要がある。
UICollectionViewFlowLayout.itemSizeUICollectionViewDelegateFlowLayout#collectionView(_:layout:sizeForItemAt:)デリゲートメソッドによるセルサイズ指定は不要。

import UIKit

final class ViewController: UIViewController {
    
    var items: [String] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        for _ in 0..<10 {
            items.append(String(repeating: "あいうえおかきくけこ", count: .random(in: 1...10)))
        }
    }
    
    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            collectionView.dataSource = self
            collectionView.delegate = self
            collectionView.alwaysBounceVertical = true
        }
    }
    
    @IBOutlet weak var flowLayout: UICollectionViewFlowLayout! {
        didSet {
            flowLayout.minimumLineSpacing = 8
            flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
            flowLayout.scrollDirection = .vertical
            flowLayout.sectionInset = .init(top: 8, left: 0, bottom: 8, right: 0)
        }
    }
}

extension ViewController: UICollectionViewDataSource {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return items.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
        cell.contentView.backgroundColor = (indexPath.item % 2 == 0) ? .systemGray4 : .systemGray6
        let item = items[indexPath.item]
        cell.titleLabel.text = item
        cell.maxWidth = collectionView.bounds.width - 16
        return cell
    }
}

extension ViewController: UICollectionViewDelegate {
    
}

class CollectionViewCell: UICollectionViewCell {
    
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var maxWidthConstraint: NSLayoutConstraint!
    
    var maxWidth: CGFloat? {
        didSet {
            guard let maxWidth = maxWidth else { return }
            maxWidthConstraint.constant = maxWidth
        }
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        
        // iOS12のみ以下制約をつけないとAutoLayoutが効かない
        contentView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            contentView.leftAnchor.constraint(equalTo: leftAnchor),
            contentView.rightAnchor.constraint(equalTo: rightAnchor),
            contentView.topAnchor.constraint(equalTo: topAnchor),
            contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
    }
}