【UIKit】CompositionalLayoutを使ったカルーセル
はじめに
UICollectionViewCompositionalLayoutを使って、要素ごとにページングするカルーセルを実装します。
実装
NSCollectionLayoutSection
enum Section {
case large, landscape, square
private var horizontalInset: CGFloat { 16 }
func layoutSection(frame: CGRect) -> NSCollectionLayoutSection {
switch self {
case .large:
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let width = frame.width - horizontalInset * 2
let groupSize = NSCollectionLayoutSize(
// 💡
widthDimension: .absolute(width),
heightDimension: .absolute(width * 0.7)
)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let layoutSection = NSCollectionLayoutSection(group: group)
// 💡
layoutSection.orthogonalScrollingBehavior = .groupPagingCentered
layoutSection.interGroupSpacing = 8
return layoutSection
case .landscape:
// ...
return layoutSection
case .square:
// ...
return layoutSection
}
}
}
全体コード
enum Section {
case large, landscape, square
private var horizontalInset: CGFloat { 16 }
func layoutSection(frame: CGRect) -> NSCollectionLayoutSection {
switch self {
case .large:
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let width = frame.width - horizontalInset * 2
// 💡
let groupSize = NSCollectionLayoutSize(
widthDimension: .absolute(width),
heightDimension: .absolute(width * 0.7)
)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let layoutSection = NSCollectionLayoutSection(group: group)
// 💡
layoutSection.orthogonalScrollingBehavior = .groupPagingCentered
layoutSection.interGroupSpacing = 8
return layoutSection
case .landscape:
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
// 💡
widthDimension: .fractionalWidth(0.6),
heightDimension: .fractionalWidth(0.6 / 2)
)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let layoutSection = NSCollectionLayoutSection(group: group)
// 💡
layoutSection.orthogonalScrollingBehavior = .groupPaging
layoutSection.interGroupSpacing = 8
layoutSection.contentInsets = .init(
top: 0, leading: horizontalInset, bottom: 0, trailing: horizontalInset
)
return layoutSection
case .square:
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
// 💡
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.4),
heightDimension: .fractionalWidth(0.4)
)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let layoutSection = NSCollectionLayoutSection(group: group)
// 💡
layoutSection.orthogonalScrollingBehavior = .groupPaging
layoutSection.interGroupSpacing = 8
layoutSection.contentInsets = .init(
top: 0, leading: horizontalInset, bottom: 0, trailing: horizontalInset
)
return layoutSection
}
}
}
3つのセクションそれぞれのNSCollectionLayoutSection
を作っています。
orthogonalScrollingBehavior
に.groupPagingCentered
または.groupPaging
を設定することで、コンテンツが慣性スクロールせず、groupごとにページングされるカルーセルの挙動になります。
さらにカルーセルらしく隣の要素がちょっと見えるようにするために、以下を設定します。
-
orthogonalScrollingBehavior
が.groupPagingCentered
の場合- groupの
NSCollectionLayoutSize
のwidthDimension
に割合を渡す。- ex.
.fractionalWidth(0.9)
- ex.
- groupの
NSCollectionLayoutSize
のwidthDimension
に固定値を渡す。- ex.
.absolute(view.frame.width - 32)
- ex.
- groupの
-
orthogonalScrollingBehavior
が.groupPaging
の場合-
NSCollectionLayoutSection
のcontentInsets
の水平方向を指定する。- ex.
NSDirectionalEdgeInsets(top: 0, leading: 32, bottom: 0, trailing: 32)
- ex.
-
今回はorthogonalScrollingBehavior
が.groupPaging
のセクションと.groupPagingCentered
のセクションとで見えている隣の要素の部分のptを同じにしたいため、.groupPagingCentered
のセクションでは親の横幅を使ってabsolute(_:)
を渡しています。
UIViewController
final class ViewController: UIViewController {
private let sections: [Section] = [
.large,
.landscape,
.square
]
private lazy var collectionView: UICollectionView = {
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 32
let collectionViewLayout = UICollectionViewCompositionalLayout(
sectionProvider: { [weak self] (index, environment) in
guard let self else { return nil }
// 💡
return self.sections[index].layoutSection(frame: self.view.frame)
},
configuration: config
)
let collectionView = UICollectionView(
frame: .zero,
// 💡
collectionViewLayout: collectionViewLayout
)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.dataSource = self
collectionView.register(
CarouselContentCell.self, forCellWithReuseIdentifier: String(describing: CarouselContentCell.self)
)
return collectionView
}()
// ...
}
全体コード
final class ViewController: UIViewController {
private let sections: [Section] = [
.large,
.landscape,
.square
]
private lazy var collectionView: UICollectionView = {
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 32
let collectionViewLayout = UICollectionViewCompositionalLayout(
sectionProvider: { [weak self] (index, environment) in
guard let self else { return nil }
// 💡
return self.sections[index].layoutSection(frame: self.view.frame)
},
configuration: config
)
let collectionView = UICollectionView(
frame: .zero,
// 💡
collectionViewLayout: collectionViewLayout
)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.dataSource = self
collectionView.register(CarouselContentCell.self, forCellWithReuseIdentifier: String(describing: CarouselContentCell.self))
return collectionView
}()
private let colors: [UIColor] = [.systemMint, .systemTeal, .systemCyan, .systemBlue, .systemIndigo]
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
}
extension ViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
sections.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
colors.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CarouselContentCell.self), for: indexPath) as? CarouselContentCell
else { return .init() }
cell.configure(backgroundColor: colors[indexPath.row])
return cell
}
}
UICollectionViewCompositionalLayout
のsectionProvider
の戻り値に、定義したNSCollectionLayoutSection
を返します。
UICollectionViewCompositionalLayout
を使ってUICollectionView
を初期化します。
UICollectionViewCell
全体コード
final class CarouselContentCell: UICollectionViewCell {
private lazy var carouselContentView: UIView = {
let carouselContentView = UIView()
carouselContentView.translatesAutoresizingMaskIntoConstraints = false
carouselContentView.layer.cornerRadius = 16
return carouselContentView
}()
func configure(backgroundColor: UIColor) {
carouselContentView.backgroundColor = backgroundColor
}
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(carouselContentView)
NSLayoutConstraint.activate([
carouselContentView.topAnchor.constraint(equalTo: contentView.topAnchor),
carouselContentView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
carouselContentView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
carouselContentView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
一般的なUICollectionViewCellの実装なので割愛します。
さいごに
UICollectionViewCompositionalLayoutを使って、プロパティの設定だけでカルーセルを実装することができました。
Discussion