Qiita の仕様上コード表示の横幅に限度があるため、改行を多用しております。ご了承ください。
仕様
上記のような画面構成が要求されたとして説明していきます。重要な制約をまとめると以下のとおりです。
- 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 を一番大きなものに統一する処理に無駄があるため、改善の余地がありそうですが、このような処理で一旦実現が可能です。