LoginSignup
13
12

More than 5 years have passed since last update.

【Swift 4対応】UICollectionView で Cell の高さ計算が複雑な画面を作成する

Last updated at Posted at 2018-07-22

Qiita の仕様上コード表示の横幅に限度があるため、改行を多用しております。ご了承ください。

仕様

qiita.001.png
上記のような画面構成が要求されたとして説明していきます。重要な制約をまとめると以下のとおりです。

  • UILabel は改行可能
  • UIImageView は正方形で、端末サイズによって可変(Cell 間隔を 16px として一行に3つ配置)
  • CollectionViewCell の height は UIImageView と UILabel の height によって決定される
  • CollectionViewCell の width は UIImageView の width によって決定される
  • Cell の height が一行3つの中で同じでない場合、一番高いものに合わせる

方針

UIImageView の width (正方形なので自動的に height も決まる) と UILabel の height を求めて CollectionViewCell の width, height を求める。

UILabel の高さを算出

Extension

import Foundation

public extension String {
    public func labelHeight(width: CGFloat,
                            font: UIFont,
                            lineBreakMode: NSLineBreakMode = .byWordWrapping) -> CGSize {
        let size = CGSize(width: width, height: .greatestFiniteMagnitude)
        let style = NSMutableParagraphStyle()
        style.lineBreakMode = lineBreakMode
        return (self as NSString).boundingRect(with: size,
                                               options: [.usesLineFragmentOrigin,
                                                         .usesFontLeading],
                                               attributes: [.font: font,
                                                            .paragraphStyle: style],
                                               context: nil).size
    }
}

Usage

let labelHeight = titleText.labelHeight(width: width,
                                        font: style.title.font,
                                        lineBreakMode: .byCharWrapping).height +
                  otherText.labelHeight(width: width,
                                        font: style.other.font,
                                        lineBreakMode: .byCharWrapping).height

取得したデータから UILabel の height を取得することができます。なお、引数には UILabel の width とフォントの指定が必須です。任意で改行方法を指定してください。今回は UILabel の width は UIImageView の width と同じになります。

UIImageView のサイズを算出

let screenWidth = UIScreen.main.bounds.size.width
let width = (screenWidth - Const.cellMargin * 4) / CGFloat(3)

余白を除いた width を計算し、それを一行に表示する個数で割れば求められます。

実装

enum Const {
    static let cellMargin: CGFloat = 16.0
    static let rowCount = 3
    static let imageToLabelMargin: CGFloat = 8.0
    static let labelToLabelMargin: CGFloat = 4.0
}

func collectionView(_ collectionView: UICollectionView,
                    layout collectionViewLayout: UICollectionViewLayout,
                    sizeForItemAt indexPath: IndexPath) -> CGSize {
    let screenWidth = UIScreen.main.bounds.size.width

    switch indexPath.section {
    case Section.content.rawValue:
        var width = (screenWidth - Const.cellMargin * 4) / CGFloat(Const.rowCount)
        let style = CollectionViewCell.LabelStyle.self
        var diffFromLeft = 0
        if indexPath.row % 3 == 0 {
            // 左端のセル
            diffFromLeft = 0
        } else if (indexPath.row - 1) % 3 == 0 {
            // 中央のセル
            diffFromLeft = -1
        } else if (indexPath.row + 1) % 3 == 0 {
            // 右端のセル
            diffFromLeft = -2
        }
        var rowLabelHeight: CGFloat = 0.0
        for value in 0...2 {
            let searchIndex = indexPath.row + value + diffFromLeft
            guard viewModel.list.count > searchIndex else {
                let height = width +
                             rowLabelHeight +
                             Const.imageToLabelMargin +
                             Const.labelToLabelMargin
                return CGSize(width: width, height: height)
            }
            let content = viewModel.content(at: searchIndex)
            let titleText = content.title
            let otherText = content.other
            let labelHeight = titleText.labelHeight(width: width,
                                                    font: style.title.font,
                                                    lineBreakMode: .byCharWrapping).height +
                otherText.labelHeight(width: width,
                                      font: style.other.font,
                                      lineBreakMode: .byCharWrapping).height
            if rowLabelHeight < labelHeight {
                rowLabelHeight = labelHeight
            }
        }

        let height = width +
                     rowLabelHeight +
                     Const.imageToLabelMargin +
                     Const.labelToLabelMargin
        return CGSize(width: width, height: height)
    default:
        return .zero
    }
}

func collectionView(_ collectionView: UICollectionView,
                    layout collectionViewLayout: UICollectionViewLayout,
                    minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
    switch section {
    case Section.content.rawValue:
        return Const.cellMargin
    default:
        return 0
    }
}

func collectionView(_ collectionView: UICollectionView,
                    layout collectionViewLayout: UICollectionViewLayout,
                    minimumLineSpacingForSectionAt section: Int) -> CGFloat {
    return 0
}

func collectionView(_ collectionView: UICollectionView,
                    layout collectionViewLayout: UICollectionViewLayout,
                    insetForSectionAt section: Int) -> UIEdgeInsets {
    switch section {
    case Section.content.rawValue:
        return UIEdgeInsets(top: Const.cellMargin,
                            left: Const.cellMargin,
                            bottom: Const.cellMargin,
                            right: Const.cellMargin)
    default:
        return .zero
    }
}

一行すべての Cell の height を一番大きなものに統一する処理に無駄があるため、改善の余地がありそうですが、このような処理で一旦実現が可能です。

13
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
12