Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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 を一番大きなものに統一する処理に無駄があるため、改善の余地がありそうですが、このような処理で一旦実現が可能です。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした